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

构建体积更小的镜像

一个镜像是由很多层(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指令执行过程中的缓存文件的典型用法示例:

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
FROM alpine:3.14

ENV LANG=C.UTF-8

# Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default.

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
    ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
    ALPINE_GLIBC_PACKAGE_VERSION="2.33-r0" && \
    ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
    echo \
        "-----BEGIN PUBLIC KEY-----\
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
        y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
        tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
        m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
        KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
        Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
        1QIDAQAB\
        -----END PUBLIC KEY-----" | sed 's/   */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
    wget \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    apk add --no-cache \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    \
    rm "/etc/apk/keys/sgerrand.rsa.pub" && \
    /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \
    echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
    \
    apk del glibc-i18n && \
    \
    rm "/root/.wget-hsts" && \
    apk del .build-dependencies && \
    rm \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"

多阶段构建

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]  

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]  

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

总结

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

参考