多阶段构建

多阶段构建对于任何试图在保持 Dockerfile 易于阅读和维护的同时优化 Dockerfile 的人都很有用。

使用多阶段构建

使用多阶段构建,您可以在 Dockerfile 中使用多个 FROM 语句。 每个 FROM 指令可以使用不同的基础,它们中的每一个都开始构建的一个新阶段。您可以选择性地从一个阶段复制工件到另一个阶段,留下您在最终镜像中不需要的一切。

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

# 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 .

最终结果是一个非常小的生产镜像,其中只包含Binaries。 生成的应用程序镜像中不包含任何构建应用程序所需的构建工具。

它是如何工作的?第二个 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 仓库中可用的标签,或标签 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 无关,因此跳过。

$ 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 不依赖它。