翻译·原文地址

多阶段构建是一个新功能,要求守护程序和客户端上的 Docker 为 17.05 或更高版本。多阶段构建对于那些努力优化 Dockerfile 同时使其易于阅读和维护的人来说非常有用。

致谢:特别感谢 Alex Ellis 允许使用他的博客文章 Builder pattern vs. Multi-stage builds in Docker 作为以下示例的基础。

一、在多阶段构建之前

构建镜像最具挑战性的一点是保持镜像大小不变。Dockerfile 中的每条指令都为镜像添加了一个层,你需要记着在移动到下一层之前清理任何不需要的工件。为了编写一个真正高效的 Dockerfile,传统上需要使用 shell 技巧和其它逻辑来保持层尽可能小,并确保每个层都具有来自前一层的所需工件而没有其它无关紧要的东西。

实际上非常普遍的是,一个 Dockerfile 用于开发(包含构建应用所需的所有内容),一个用于生产的精简版 Dockerfile - 它只包含你的应用以及运行它所需的内容。这被称为“构建器模式”。维护两个 Dockerfiles 并不理想。

这是一个 Dockerfile.buildDockerfile 的例子,它遵循上面的构建器模式:

Dockerfile.build

FROM golang:1.7.3
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 .

注意,此示例还使用 Bash && 运算符人为压缩两个 RUN 命令,以避免在镜像中创建其它层。 这很容易出错并且难以维护。例如,很容易插入另一个命令并忘记使用 \ 字符续行。

Dockerfile

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]  

build.sh

#!/bin/sh
echo Building alexellis2/href-counter:build

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  
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app  
docker container rm -f extract

echo Building alexellis2/href-counter:latest

docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app

当你运行 build.sh 脚本时,它需要构建第一个镜像,从中创建容器以复制工件,然后构建第二个镜像。 两个镜像都占用了你的系统空间,并且提取 app 工件占用了你的本地磁盘。

多阶段构建大大简化了这种情况!

二、使用多阶段构建

对于多阶段构建,你可以在 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令可以使用不同的基础,并且每个指令都开始构建的新阶段。你可以选择性地将工件从一个阶段复制到另一个阶段,从而在最终镜像中丢下你不想要的所有内容。为了说明这是如何工作的,让我们调整上一节中的 Dockerfile 以使用多阶段构建。

Dockerfile

FROM golang:1.7.3
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"]  

你只需要单个 Dockerfile。你也不需要单独的构建脚本。只需运行 docker build

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

最终结果是与以前相同的极小的生产镜像,复杂性显着降低。你不需要创建任何中间镜像,也不需要将任何工件提取到本地系统。

它是如何工作的?第二个 FROM 指令以 alpine:latest 镜像为基础开始一个新的构建阶段。COPY --from=0 行仅将前一阶段的构建工件复制到新阶段。 Go SDK 和任何中间工件都被丢弃,而不是保存在最终镜像中。

三、给你的构建阶段命名

默认情况下,阶段未命名,你可以通过整数来引用它们,以第一个 FROM 指令为 0 开始。但是,你可以通过向 FROM 指令添加 as <NAME> 来命名你的构建阶段 此示例通过给阶段命名并在 COPY 指令中使用该命名称来改进前一个示例。这意味着即使稍后重新排序 Dockerfile 中的指令,COPY 也不会中断。

FROM golang:1.7.3 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"]  

四、停在特定的构建阶段

构建镜像时,不一定需要构建包含每个阶段的完整 Dockerfile。你可以指定目标构建阶段。以下命令假定你使用的是前面的 Dockerfile,但在名为 builder 的阶段停止:

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

在一些可能的场景中这非常有用:

  • 调试特定的构建阶段
  • debug 阶段启用所有调试标记或工具,但在 production 阶段精简
  • testing 阶段中,在你的应用中填充测试数据,但在另一个不同的阶段使用真实数据来构建产品

五、使用外部镜像作为“舞台”

使用多阶段构建时,不会限制你只从预先在 Dockerfile 中创建的阶段进行复制。你可以使用 COPY --from 指令从单独的镜像复制,无论使用本地镜像名称,本地可用的 tagtag ID,还是 Docker Registry 中的。 如有必要,Docker 客户端会拉取镜像并从那里复制工件。语法是:

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
(完)