多阶段构建

多阶段构建对于那些努力优化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注册表上可用的标签或标签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不依赖于它。