目录

Docker镜像优化之多阶段构建

在多阶段构建之前

在刚接触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