多阶段
本节探讨多阶段构建。使用多阶段构建有两个主要原因
- 它们允许您并行运行构建步骤,使您的构建流程更快、更高效。
- 它们允许您创建一个占用空间更小的最终镜像,其中只包含运行程序所需的组件。
在 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
)。
您还可以为二进制文件构建阶段命名,并在将二进制文件复制到最终的 scratch
镜像时,在 COPY --from=stage_name
指令中引用阶段名称。
# 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
现在镜像更小了,每个大约 4MB。
此更改还避免了每次都要构建两个二进制文件。当选择构建 client
目标时,Docker 只构建通向该目标的阶段。如果不需要 build-server
和 server
阶段,则会跳过它们。同样,构建 server
目标会跳过 build-client
和 client
阶段。
总结
多阶段构建对于创建占用空间更小、更精简的镜像非常有用,还有助于加快构建速度。
相关信息
下一步
下一节将介绍如何使用文件挂载来进一步提高构建速度。