在构建容器镜像时,会用到一些技巧以构建出体积更小的镜像,以在镜像分发和容器部署时获得更快的速度。

构建体积更小的镜像

一个镜像是由很多层(Layers)组成的,Dockerfile中的每条指令都会创建镜像层,但只有RUN, COPY, ADD会使镜像的体积增加。 理解了镜像层后,显而易见构建体积更小的镜像的需要从两个方面着手: 精简镜像层数精简镜像每层的大小

精简镜像层数的方法有以下两种:

  • 多个RUN指令合并为一个
  • 使用多阶段构建( multi-stage build)

精简镜像每层大小的方法有以下两种:

  • 选择更小的基础镜像例如alpine。当然这个要做好取舍,过小的基础镜像例如官方的scratch镜像构建出来的镜像可能只包含运行程序必须的东西,使用起来可能不方便调试,例如因为没有shell而无法exec到容器内部。推荐第25节《编写Dockerfile的一些实践经验》中介绍的基于alpine的alpine-glibc基础镜像。
  • 删除RUN指令执行过程中的缓存文件。多个RUN指令合并为一个,RUN指令中的多个shell命令中间执行过程中产生的缓存文件需要在此RUN指令结束之前删除。

下面的Dockerfile就是,多个RUN命令合并为一个删除RUN指令执行过程中的缓存文件的典型用法示例:

 1FROM alpine:3.14
 2
 3ENV LANG=C.UTF-8
 4
 5# Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default.
 6
 7RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
 8    ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
 9    ALPINE_GLIBC_PACKAGE_VERSION="2.33-r0" && \
10    ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
11    ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
12    ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
13    apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
14    echo \
15        "-----BEGIN PUBLIC KEY-----\
16        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
17        y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
18        tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
19        m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
20        KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
21        Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
22        1QIDAQAB\
23        -----END PUBLIC KEY-----" | sed 's/   */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
24    wget \
25        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
26        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
27        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
28    apk add --no-cache \
29        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
30        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
31        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
32    \
33    rm "/etc/apk/keys/sgerrand.rsa.pub" && \
34    /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \
35    echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
36    \
37    apk del glibc-i18n && \
38    \
39    rm "/root/.wget-hsts" && \
40    apk del .build-dependencies && \
41    rm \
42        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
43        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
44        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"

多阶段构建

使用多阶段构建不仅可以减小镜像的体积,还可以保证Dockerfile内容的可读性和可维护性。

多阶段构建是在一个Dockerfile中使用多个FROM指令,每个FROM指令可以使用不同的基础镜像,每个FROM会对应一个阶段(stage),多阶段构建就是使用多个FROM,下面是一个例子:

 1FROM golang:1.16
 2WORKDIR /go/src/github.com/alexellis/href-counter/
 3RUN go get -d -v golang.org/x/net/html  
 4COPY app.go ./
 5RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
 6
 7FROM alpine:latest  
 8RUN apk --no-cache add ca-certificates
 9WORKDIR /root/
10COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
11CMD ["./app"]  

上面第10行COPY --from=0 是指从阶段0(stage 0)拷贝文件,将阶段0中使用go build出的二进制文件拷贝到阶段1中。 多阶段构建的Dockerfile本质上就是使用FROM指令将一个Dockerfile中的构建内容分成了多个。

还可以为每个Stage命名,这样比0,1,2这样的序号更具有可读性:

 1FROM golang:1.16 AS builder
 2WORKDIR /go/src/github.com/alexellis/href-counter/
 3RUN go get -d -v golang.org/x/net/html  
 4COPY app.go    ./
 5RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
 6
 7FROM alpine:latest  
 8RUN apk --no-cache add ca-certificates
 9WORKDIR /root/
10COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
11CMD ["./app"]  

上面的将构建分成两个阶段,第一阶段为builder,只是使用golang:1.16镜像中的go语言SDK构建出项目源码的可执行文件,第二阶段基于将第一阶段使用go build出的二进制文件拷贝到alpine:latest镜像中。 最终构建出的镜像不会包含golang:1.16本身,而只是alpine镜像的基础上叠加二进制文件,保持了镜像体积,因为go语言是静态编译的,依赖都会被放到构建出的二进制文件中,运行这个二进制文件不需要Go语言SDK本身。

总结

通过本节我们学习了保持构建小体积镜像的一些方法。构建体积更小的镜像的需要从两个方面着手: 精简镜像层数精简镜像每层的大小。 精简镜像层数的方法有: 多个RUN指令合并为一个使用多阶段构建( multi-stage build)。 精简镜像每层大小的方法有:选择更小的基础镜像多个RUN指令合并为一个后,应该删除RUN指令执行过程中的缓存文件

参考