在多阶段构建之前
在刚接触Docker的时候,就被其口号:Build,Ship,and Run Any App,Anywhere所吸引。一次构建,到处运行。后面真正使用的时候才发现,控制镜像大小是一件值得挑战的事情。
镜像的选择
举了例子,用docker pull
命令分别去拉取golang:1.18和golang:1.18-alpine这两个镜像,可以发现这两个镜像的大小差距很大。
1
2
3
4
5
6
7
8
9
10
11
12
|
$ docker pull golang:1.18
1.18: Pulling from library/golang
bbeef03cda1f: Already exists
f049f75f014e: Already exists
56261d0e6b05: Already exists
9bd150679dbd: Already exists
bfcb68b5bd10: Already exists
06d0c5d18ef4: Already exists
cc7973a07a5b: Already exists
Digest: sha256:50c889275d26f816b5314fc99f55425fa76b18fcaf16af255f5d57f09e1f48da
Status: Downloaded newer image for golang:1.18
docker.io/library/golang:1.18
|
1
2
3
4
5
6
7
8
9
|
$ docker pull golang:1.18-alpine
1.18-alpine: Pulling from library/golang
8921db27df28: Pull complete
a2f8637abd91: Pull complete
4ba80a8cd2c7: Pull complete
dbc2308a4587: Pull complete
Digest: sha256:77f25981bd57e60a510165f3be89c901aec90453fd0f1c5a45691f6cb1528807
Status: Downloaded newer image for golang:1.18-alpine
docker.io/library/golang:1.18-alpine
|
1
2
3
4
|
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
golang 1.18 c37a56a6d654 8 months ago 965MB
golang 1.18-alpine a77f45e5f987 8 months ago 330MB
|
Alpine Linux是一个轻量级的Linux发行版,它的镜像大小只有5MB左右,因此在Docker容器化的应用中得到了广泛的应用。
缺点是相比其他主流的Linux发行版,其社区和用户群体要小很多。这意味着在使用Alpine Linux时可能会遇到一些特定的问题,需要自己解决或者寻求社区的帮助。
指令的优化
像 RUN、COPY、ADD 指令都会在镜像里增加一个层,并且在进入下一个层时,要记得移除当前层产生的产物,否则会导致镜像变得过于大。常用的方法就是把一些命令通过&&
链接,减少构建层数,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# 镜像源
FROM golang:1.18-alpine
# 设定时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
#为我们的镜像设置必要的环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64 \
GOPROXY="https://goproxy.cn,direct"
#创建工作目录
RUN mkdir /app && mkdir -p /data/ProjectLog
#切换工作目录
WORKDIR /app
#添加项目文件
ADD . /app
#下载依赖并编译程序
RUN go mod tidy && go build -o main ./main.go
#最终运行docker的命令
CMD /app/main
|
使用多阶段构建
多阶段构建Docker版本要求
Docker Engine 17.05开始引入多阶段构建,需要升级到以上版本
对于多阶段构建,你可以在Dockerfile中使用多个FROM语句。每个FROM指令都可以使用不同的基镜像,并且它们都开始了构建的新阶段。你可以有选择地将中间物从一个阶段复制到另一个阶段,舍弃在最后的镜像中不想要的所有内容。
demo案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
# 镜像源
FROM golang:1.18-alpine as build
# 设定时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
#为我们的镜像设置必要的环境变量
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64 \
GOPROXY="https://goproxy.cn,direct"
#创建工作目录
RUN mkdir /app
#切换工作目录
WORKDIR /app
#添加项目文件
ADD main.go /app
#下载依赖并编译程序
RUN go mod init multi-build && go mod tidy && go build -o main ./main.go
# 二段构建
FROM alpine:latest
COPY --from=build /app/main ./
EXPOSE 8080
#最终运行docker的命令
CMD ./main
|
以上是一个多段构建的 demo ,大致逻辑如下:
- FROM拉取一个golang:1.18-alpine镜像,并命名为build
- 设置时区以及一些go开发环境的配置
- 把代码COPY到镜像的工作目录中
- 初始化mod,下载依赖并进行编译
- (重点)FROM开始另外一段镜像的构建
- 通过–from命令把上一段编译好的执行程序main,COPY到alpine:latest镜像的根目录下
- 暴露8080端口用于后续做宿主机端口映射
- CMD命令在运行容器后执行main程序
build镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
$ docker build . -t multi-build
[+] Building 26.0s (14/14) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 694B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 9.7s
=> [internal] load metadata for docker.io/library/golang:1.18-alpine 0.0s
=> [build 1/6] FROM docker.io/library/golang:1.18-alpine 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 29B 0.0s
=> [stage-1 1/2] FROM docker.io/library/alpine:latest@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 16.0s
=> => resolve docker.io/library/alpine:latest@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 0.0s
=> => sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978 1.64kB / 1.64kB 0.0s
=> => sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 528B / 528B 0.0s
=> => sha256:8ca4688f4f356596b5ae539337c9941abc78eda10021d35cbc52659c74d9b443 1.47kB / 1.47kB 0.0s
=> => sha256:96526aa774ef0126ad0fe9e9a95764c5fc37f409ab9e97021e7b4775d82bf6fa 3.40MB / 3.40MB 15.7s
=> => extracting sha256:96526aa774ef0126ad0fe9e9a95764c5fc37f409ab9e97021e7b4775d82bf6fa 0.2s
=> CACHED [build 2/6] RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone 0.0s
=> CACHED [build 3/6] RUN mkdir /app 0.0s
=> CACHED [build 4/6] WORKDIR /app 0.0s
=> CACHED [build 5/6] ADD main.go /app 0.0s
=> [build 6/6] RUN go mod init multi-build && go mod tidy && go build -o main ./main.go 5.0s
=> [stage-1 2/2] COPY --from=build /app/main ./ 0.1s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:dae5ed2ab7a7aa7fbdcda7f199889a694c9e88d9af6059c905b7218972f8ec23 0.0s
=> => naming to docker.io/library/multi-build
|
可以看到,通过这种方式构建出来的镜像,只有十多M,比一开始提到的几百M镜像大小差距是非常大的。
1
2
3
|
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
multi-build latest dae5ed2ab7a7 11 seconds ago 13.6MB
|
运行容器,程序能正常访问
1
2
3
4
|
$ docker run --name=docker-multi-build -d -it -p 8080:8080 multi-build
557e054e38f3475e32e99551de4ea46c37969ad392dcf63675fa176e398cc1a2
$ curl http://localhost:8080/hello
Hello!%
|
对构建阶段进行命名
默认情况下,每个阶段是没有命名的,默认从整数0开始命名,可以通过--from=
指令进行引用,即
1
2
3
4
5
6
7
|
# 镜像源
FROM golang:1.18-alpine
...
# 二段构建
FROM alpine:latest
COPY --from=0 /app/main ./
...
|
也可以通过as <name>
指令进行命名,如下
1
2
3
4
5
6
7
|
# 镜像源
FROM golang:1.18-alpine as build
...
# 二段构建
FROM alpine:latest
COPY --from=build /app/main ./
...
|
引用外部镜像进行构建
使用多阶段构建时,不仅限于从Dockerfile中之前创建的阶段进行复制。 还可以使用COPY –from指令从单独的镜像中复制,可以是本地镜像名称,本地或Docker注册中心上可用的标签,或一个标签ID。 Docker客户端在必要时拉取镜像并从中复制构件。 语法为:
1
|
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
|