docker多阶段构建

多阶段的构建对于那些努力优化 Dockerfiles 的开发者来说非常有用,同时也使 Dockerfiles 更易于阅读和维护。

1.多阶段 build 之前的 build

build镜像 最具挑战性的事情之一是使镜像变得更小。Dockerfile 中的每条指令都会为镜像添加一层,需要记住的是要清理任何不需要的内容,然后再进入下一层。为了编写一个非常有效的 Dockerfile,传统上需要采用shell技巧和其他逻辑来保持每一层尽可能小,并确保每一层都具有上一层中所需的内容,而没有其他多余内容。

实际上,一个用于生产环境的 Dockerfile(其中包含构建应用程序所需的一切),其中仅包含您的应用程序以及运行它所需的内容,并尽可能的小。这被称为 “builder pattern”。而维持两个 Dockerfiles 也不是理想的选择。

下面举一个使用 Dockerfile.build 与 Dockerfile 进行构建的例子。

Dockerfile.build:

1
2
3
4
5
6
# syntax=docker/dockerfile:1
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go ./
RUN go get -d -v golang.org/x/net/html \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

注意,此示例使用了shell的 &&运算符 人为地将多个运行命令一起压缩,以避免在图像中创建附加层,这是容易失败且难以维护。例如,很容易插入另一个命令,而忘记使用换行 \ 字符。

Dockerfile:

1
2
3
4
5
6
# syntax=docker/dockerfile:1
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app ./
CMD ["./app"]

build.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh
echo Building alexellis2/href-counter:build

# 执行第一个 Dockerfile 生成一个镜像
docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \
-t alexellis2/href-counter:build . -f Dockerfile.build

# 从上面生成的镜像中启动一个容器
docker container create --name extract alexellis2/href-counter:build
# 从容器中拷贝文件app 到 本地
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app
# 移除容器
docker container rm -f extract

echo Building alexellis2/href-counter:latest

# 执行第二个 Dockerfile 生成最终的镜像
docker build --no-cache -t alexellis2/href-counter:latest .

# 删除从容器中拷贝到本地的中间文件
rm ./app

运行 build.sh脚本时,需要经过几个主要步骤:
① 构建第一个镜像
② 基于第一个镜像生成一个容器,然后把容器中所需要的文件拷贝到本地
③ 构建第二个镜像,并把第一个镜像容器中拷贝出来的文件拷贝进去
④ 删除从容器中拷贝到本地的中间文件

最终发现,两个镜像不仅占用系统空间,还需要把容器中所需要的文件拷贝到本地磁盘,比较繁琐并占用空间。

2.多阶段 builds

使用多阶段构建,可以在Dockerfile中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的基础镜像,并开始了构建的新阶段。同时,可以选择性地将文件从一个build阶段复制到另一个build阶段,最终镜像中只保留想要的文件内容。

为了展示其工作原理,我们调整上面的 Dockerfile,以进行多阶段构建。

Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# syntax=docker/dockerfile:1

# 构建阶段1
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 .


# 构建阶段2
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 从构建阶段1 拷贝文件到 构建阶段2
COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./

CMD ["./app"]

最终的构建只要一个 Dockerfile 文件,并且不再需要 build.sh,仅仅是执行一个 docker build 命令而已。

1
docker build -t alexellis2/href-counter:latest .

最终结果是得到了比以前更小的镜像,其复杂性也显着降低。同时无需创建任何中间镜像,也无需在本地存储任何临时文件。

它是如何工作的呢?
从第二个 FROM 指令开始了一个新的构建阶段,该阶段使用镜像 alpine:latest 作为其基础。使用 COPY --from=0 从第一阶段构建拷贝所需要的文件。最终 GO SDK 和任何中间文物都被丢弃,并且没有保存在最终镜像中。

2.1 为构建阶段命名

默认情况下阶段未命名,以其整数编号为单位,第一个 FROM 指令从 0 开始。但是,您可以通过在 FROM 后面添加 AS 来命名当前构建阶段。
下面将演示如何通过 构建阶段命名 来改善上面一个例子。这意味着,即使 Dockerfile 中构建阶段稍后会进行重排序,COPY 指令也不会因为构建阶段重排序而导致拷贝错误文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# syntax=docker/dockerfile:1

# 构建阶段1 命名为 builder
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 .

# 构建阶段2
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

# 指明从 builder 拷贝文件
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]

2.2 终止特定的构建阶段

构建镜像时,不一定需要构建包括每个阶段在内的整个 Dockerfile。您可以指定目标构建阶段。
以下命令使用上面的 Dockerfile,在构建过程时最终停止在名为 Builder 的阶段。

1
docker build --target builder -t alexellis2/href-counter:latest .

一些可能非常有用的情况:

  • 调试特定的构建阶段
  • 使用启用所有调试符号或工具的调试阶段,以及精益生产阶段
  • 使用测试阶段,其中应用程序填充了测试数据,但使用其他阶段构建生产的阶段,该阶段使用真实数据

2.3 使用外部镜像作为一个构建

当使用多阶段构建时,不仅限于从同一个 Dockerfile 中赋值前面构建阶段产生的文件。也可以使用 COPY --from 从单独的镜像中复制,也可以是本地镜像名称、本地tag名称 或者 Docker仓库 或者 一个 tag ID。Docker 客户端将会在必要时拉取镜像,并从那里复制文件内容。语法如下:

1
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

2.4 用前一个build阶段用作为新的build阶段

可以在使用 FROM 时引用前面 build 阶段,例如:

1
2
3
4
5
6
7
8
9
10
11
# syntax=docker/dockerfile:1
FROM alpine:latest AS builder
RUN apk --no-cache add build-base

FROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp

FROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp

3.版本兼容

多阶段构建是在 Docker 17.05开始引入的。

本文翻译自 docker 官方文档

4.拓展阅读