Skip to content

多阶段构建

原文:https://docs.docker.com/build/guide/multi-stage/

本节探讨多阶段构建。使用多阶段构建的两个主要原因是:

  • 它们允许您并行运行构建步骤,使您的构建管道更快更高效。
  • 它们允许您创建一个体积更小的最终镜像,仅包含运行程序所需的内容。

在 Dockerfile 中,一个构建阶段由 FROM 指令表示。上一节中的 Dockerfile 没有利用多阶段构建。它只有一个构建阶段。这意味着最终镜像中充满了用于编译程序的资源。

console
docker build --tag=buildme .
docker images buildme
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
buildme      latest    c021c8a7051f   5 seconds ago   150MB

程序编译成可执行二进制文件,因此最终镜像中不需要存在 Go 语言工具。

添加阶段

使用多阶段构建,您可以选择为您的构建和运行环境使用不同的基础镜像。您可以从构建阶段复制构建工件到运行阶段。

按以下方式修改 Dockerfile。此更改使用最小的 scratch 镜像作为基础创建另一个阶段。在最终的 scratch 阶段,之前阶段构建的二进制文件被复制到新阶段的文件系统中。

diff
  # syntax=docker/dockerfile:1
  FROM golang:1.21-alpine
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .
  RUN go build -o /bin/client ./cmd/client
  RUN go build -o /bin/server ./cmd/server
+
+ FROM scratch
+ COPY --from=0 /bin/client /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

现在如果您构建镜像并查看,应该会看到一个明显更小的数字:

console
docker build --tag=buildme .
docker images buildme
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
buildme      latest    436032454dd8   7 seconds ago   8.45MB

镜像从 150MB 减小到只有 8.45MB。这是因为最终镜像只包含二进制文件,没有其他内容。

并行性

您已经减少了镜像的体积。以下步骤展示了如何使用多阶段构建和并行性来提高构建速度。当前的构建依次产生二进制文件。没有理由必须先构建客户端再构建服务器,或反之。

您可以将构建二进制文件的步骤分为不同的阶段。在最终的 scratch 阶段,从每个相应的构建阶段复制二进制文件。通过将这些构建分为不同的阶段,Docker 可以并行运行它们。

构建每个二进制文件的阶段都需要 Go 编译工具和应用依赖。将这些公共步骤定义为可重用的基础阶段。您可以通过使用模式 FROM image AS stage_name 为阶段命名。这允许您在另一个阶段的 FROM 指令中引用阶段名称(FROM stage_name)。

您还可以为二进制构建阶段命名,并在将二进制文件复制到最终的 scratch 镜像时,引用阶段名称在 COPY --from=stage_name

令中。

diff
  # syntax=docker/dockerfile:1
- FROM golang:1.21-alpine
+ FROM golang:1.21-alpine AS base
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .
+
+ FROM base AS build-client
  RUN go build -o /bin/client ./cmd/client
+
+ FROM base AS build-server
  RUN go build -o /bin/server ./cmd/server

  FROM scratch
- COPY --from=0 /bin/client /bin/server /bin/
+ COPY --from=build-client /bin/client /bin/
+ COPY --from=build-server /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

现在,而不是先后构建二进制文件,build-clientbuild-server 阶段会同时执行。

并行执行的阶段

构建目标

最终镜像现在既小又高效地使用了并行性进行构建。但这个镜像有点奇怪,因为它在同一个镜像中包含了客户端和服务器二进制文件。这些不应该是两个不同的镜像吗?

可以使用单个 Dockerfile 创建多个不同的镜像。您可以使用 --target 标志指定构建的目标阶段。将未命名的 FROM scratch 阶段替换为两个分别命名为 clientserver 的单独阶段。

diff
  # syntax=docker/dockerfile:1
  FROM golang:1.21-alpine AS base
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .

  FROM base AS build-client
  RUN go build -o /bin/client ./cmd/client

  FROM base AS build-server
  RUN go build -o /bin/server ./cmd/server

- FROM scratch
- COPY --from=build-client /bin/client /bin/
- COPY --from=build-server /bin/server /bin/
- ENTRYPOINT [ "/bin/server" ]

+ FROM scratch AS client
+ COPY --from=build-client /bin/client /bin/
+ ENTRYPOINT [ "/bin/client" ]

+ FROM scratch AS server
+ COPY --from=build-server /bin/server /bin/
+ ENTRYPOINT [ "/bin/server" ]

现在您可以将客户端和服务器程序作为单独的 Docker 镜像(标签)构建:

console
docker build --tag=buildme-client --target=client .
docker build --tag=buildme-server --target=server .
docker images "buildme*"
REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
buildme-client   latest    659105f8e6d7   20 seconds ago   4.25MB
buildme-server   latest    666d492d9f13   5 seconds ago    4.2MB

这些镜像现在更小,每个大约 4 MB。

这一更改还避免了每次都需要构建两个二进制文件。当选择构建 client 目标时,Docker 只构建到该目标的阶段。如果不需要,build-serverserver 阶段将被跳过。同样,构建 server 目标时会跳过 build-clientclient 阶段。

总结

多阶段构建对于创建体积更小、更干净的镜像非常有用,还有助于加快构建速度。

相关信息:

下一步

下一节描述了如何使用文件挂载进一步提高构建速度。