多阶段构建
多阶段构建对于那些难以优化 Dockerfile 但又希望它们易于阅读和维护的人来说非常有用。
使用多阶段构建
借助多阶段构建,你可以在 Dockerfile 中使用多个 FROM
语句。每个 FROM
指令都可以使用不同的基础镜像,并且每个指令都开始一个新的构建阶段。你可以选择性地将制品从一个阶段复制到另一个阶段,从而在最终镜像中排除所有不需要的内容。
以下 Dockerfile 包含两个独立的阶段:一个用于构建二进制文件,另一个用于将二进制文件从第一阶段复制到下一阶段。
你只需要一个 Dockerfile。无需单独的构建脚本。只需运行 docker build
。
$ docker build -t hello .
最终结果是一个仅包含二进制文件的微小生产镜像。构建应用程序所需的任何构建工具都不包含在最终镜像中。
它是如何工作的?第二个 FROM
指令开始一个新的构建阶段,以 scratch
镜像作为其基础。COPY --from=0
行仅将之前阶段构建的制品复制到这个新阶段。Go SDK 和任何中间制品都将被丢弃,不会保存在最终镜像中。
命名你的构建阶段
默认情况下,阶段是没有名称的,你可以通过它们的整数编号来引用它们,第一个 FROM
指令的编号从 0 开始。但是,你可以通过在 FROM
指令后添加 AS <NAME>
来命名你的阶段。这个例子通过命名阶段并在 COPY
指令中使用该名称来改进了之前的例子。这意味着即使你的 Dockerfile 中的指令顺序后续被重新调整,COPY
也不会出错。
在特定构建阶段停止
当你构建镜像时,不一定需要构建包含所有阶段的整个 Dockerfile。你可以指定一个目标构建阶段。以下命令假定你正在使用之前的 Dockerfile
,但在名为 build
的阶段停止构建:
$ docker build --target build -t hello .
以下是一些可能用到此方法的场景:
- 调试特定的构建阶段
- 使用一个启用所有调试符号或工具的
debug
阶段,以及一个精简的production
阶段 - 使用一个填充了测试数据的
testing
阶段,但使用另一个使用真实数据的阶段进行生产构建
使用外部镜像作为阶段
使用多阶段构建时,你不限于从 Dockerfile 中之前创建的阶段复制。你可以使用 COPY --from
指令从单独的镜像中复制,可以使用本地镜像名称、本地或 Docker registry 上可用的标签,或标签 ID。如有必要,Docker 客户端会拉取该镜像并从中复制制品。语法如下:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
将之前的阶段用作新阶段
你可以在使用 FROM
指令时引用之前的阶段,从该阶段继续。例如:
旧版构建器和 BuildKit 之间的差异
旧版 Docker Engine 构建器会处理 Dockerfile 中直到选定 --target
的所有阶段。即使选定的目标不依赖某个阶段,它也会构建该阶段。
BuildKit 只构建目标阶段依赖的阶段。
例如,给定以下 Dockerfile:
如果 启用 BuildKit,构建此 Dockerfile 中的 stage2
目标意味着只处理 base
和 stage2
阶段。因为不依赖 stage1
,所以 stage1
会被跳过。
$ DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .
[+] Building 0.4s (7/7) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 36B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> CACHED [base 1/2] FROM docker.io/library/ubuntu 0.0s
=> [base 2/2] RUN echo "base" 0.1s
=> [stage2 1/1] RUN echo "stage2" 0.2s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f55003b607cef37614f607f0728e6fd4d113a4bf7ef12210da338c716f2cfd15 0.0s
另一方面,如果未启用 BuildKit 构建相同的目标,则会处理所有阶段。
$ DOCKER_BUILDKIT=0 docker build --no-cache -f Dockerfile --target stage2 .
Sending build context to Docker daemon 219.1kB
Step 1/6 : FROM ubuntu AS base
---> a7870fd478f4
Step 2/6 : RUN echo "base"
---> Running in e850d0e42eca
base
Removing intermediate container e850d0e42eca
---> d9f69f23cac8
Step 3/6 : FROM base AS stage1
---> d9f69f23cac8
Step 4/6 : RUN echo "stage1"
---> Running in 758ba6c1a9a3
stage1
Removing intermediate container 758ba6c1a9a3
---> 396baa55b8c3
Step 5/6 : FROM base AS stage2
---> d9f69f23cac8
Step 6/6 : RUN echo "stage2"
---> Running in bbc025b93175
stage2
Removing intermediate container bbc025b93175
---> 09fc3770a9c4
Successfully built 09fc3770a9c4
旧版构建器会处理 stage1
,即使 stage2
不依赖它。