多阶段构建

多阶段构建对于那些难以优化 Dockerfile 但又希望它们易于阅读和维护的人来说非常有用。

使用多阶段构建

借助多阶段构建,你可以在 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的基础镜像,并且每个指令都开始一个新的构建阶段。你可以选择性地将制品从一个阶段复制到另一个阶段,从而在最终镜像中排除所有不需要的内容。

以下 Dockerfile 包含两个独立的阶段:一个用于构建二进制文件,另一个用于将二进制文件从第一阶段复制到下一阶段。

# syntax=docker/dockerfile:1
FROM golang:1.23
WORKDIR /src
COPY <<EOF ./main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]

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

$ docker build -t hello .

最终结果是一个仅包含二进制文件的微小生产镜像。构建应用程序所需的任何构建工具都不包含在最终镜像中。

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

命名你的构建阶段

默认情况下,阶段是没有名称的,你可以通过它们的整数编号来引用它们,第一个 FROM 指令的编号从 0 开始。但是,你可以通过在 FROM 指令后添加 AS <NAME> 来命名你的阶段。这个例子通过命名阶段并在 COPY 指令中使用该名称来改进了之前的例子。这意味着即使你的 Dockerfile 中的指令顺序后续被重新调整,COPY 也不会出错。

# syntax=docker/dockerfile:1
FROM golang:1.23 AS build
WORKDIR /src
COPY <<EOF /src/main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=build /bin/hello /bin/hello
CMD ["/bin/hello"]

在特定构建阶段停止

当你构建镜像时,不一定需要构建包含所有阶段的整个 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 指令时引用之前的阶段,从该阶段继续。例如:

# 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

旧版构建器和 BuildKit 之间的差异

旧版 Docker Engine 构建器会处理 Dockerfile 中直到选定 --target 的所有阶段。即使选定的目标不依赖某个阶段,它也会构建该阶段。

BuildKit 只构建目标阶段依赖的阶段。

例如,给定以下 Dockerfile:

# syntax=docker/dockerfile:1
FROM ubuntu AS base
RUN echo "base"

FROM base AS stage1
RUN echo "stage1"

FROM base AS stage2
RUN echo "stage2"

如果 启用 BuildKit,构建此 Dockerfile 中的 stage2 目标意味着只处理 basestage2 阶段。因为不依赖 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 不依赖它。

页面选项