多级

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

  • 它们允许您并行运行构建步骤,使您的构建管道更快、更高效。
  • 它们允许您创建占用空间较小的最终映像,仅包含运行程序所需的内容。

在 Dockerfile 中,构建阶段由FROM指令表示。上一节中的 Dockerfile 没有利用多阶段构建。这都是一个构建阶段。这意味着最终的图像因用于编译程序的资源而变得臃肿。

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

该程序编译为可执行二进制文件,因此您不需要 Go 语言实用程序存在于最终映像中。

添加阶段

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

修改 Dockerfile 如下。此更改使用最小scratch图像作为基础创建了另一个阶段。在最后scratch阶段,前一阶段构建的二进制文件将复制到新阶段的文件系统。

  # 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" ]

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

$ 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)。

您还可以为二进制构建阶段分配一个名称,并COPY --from=stage_name在将二进制文件复制到最终scratch映像时在指令中引用阶段名称。

  # 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-client和阶段。build-server

并行执行的阶段

建立目标

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

可以使用单个 Dockerfile 创建多个不同的映像。您可以使用该标志指定构建的目标阶段--target。将未命名的阶段替换FROM scratch为两个名为client和 的 独立阶段server

  # 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 映像(标签):

$ 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-server和阶段。server同样,构建server目标会跳过build-clientclient阶段。

概括

多阶段构建对于创建更少膨胀和更小占用空间的映像非常有用,并且还有助于使构建运行得更快。

相关信息:

下一步

下一节将介绍如何使用文件挂载来进一步提高构建速度。