重学容器27: 容器镜像构建技巧之构建体积更小的镜像和多阶段构建
2021-07-26
在构建容器镜像时,会用到一些技巧以构建出体积更小的镜像,以在镜像分发和容器部署时获得更快的速度。
构建体积更小的镜像 #
一个镜像是由很多层(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指令执行过程中的缓存文件
。