Skip to content

多阶段构建

原文:https://docs.docker.com/build/building/multi-stage/#use-multi-stage-builds

多阶段构建对任何曾经努力优化 Dockerfile 同时又希望它们易于阅读和维护的人都是有用的。

使用多阶段构建

在多阶段构建中,你在 Dockerfile 中使用多个 FROM 声明。每个 FROM 指令可以使用不同的基础镜像,每一个都开始一个新的构建阶段。你可以从一个阶段选择性地复制构件到另一个阶段,把你不想要的所有东西都留在最终镜像之外。

以下 Dockerfile 分为两个单独的阶段:一个用于构建二进制文件,另一个则将二进制文件从第一阶段复制到下一个阶段。

dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.21
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

bash
docker build -t hello .

最终结果是一个只包含二进制文件的小型生产镜像。构建应用所需的任何构建工具都不包含在最终镜像中。

它是如何工作的?第二个 FROM 指令以 scratch 镜像为基础开始一个新的构建阶段。COPY --from=0 行仅复制前一阶段构建的产物到这个新阶段。Go SDK 和任何中间构件都被留下,不保存在最终镜像中。

为你的构建阶段命名

默认情况下,阶段是不命名的,你通过它们的整数编号来引用它们,从第一个 FROM 指令的 0 开始。然而,你可以通过在 FROM 指令中添加 AS <NAME> 来命名你的阶段。以下示例通过命名阶段并在 COPY 指令中使用该名称,改进了前一个示例。这意味着即使你的 Dockerfile 中的指令稍后被重新排序,COPY 也不会出错。

dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.21 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 的阶段:

bash
docker build --target build -t hello .

这可能在一些场景中很有用,例如:

  • 调试特定的构建阶段
  • 使用一个包含所有调试符号或工具的 debug 阶段,和一个精

简的 production 阶段

  • 使用一个 testing 阶段,在该阶段中你的应用被填充测试数据,但是使用不同的阶段为生产构建,使用真实数据

使用外部镜像作为一个阶段

在使用多阶段构建时,你不仅限于复制你在 Dockerfile 中较早创建的阶段。你可以使用 COPY --from 指令从一个单独的镜像复制,无论是使用本地镜像名、本地或 Docker 注册中可用的标签,还是标签 ID。如果需要,Docker 客户端会拉取镜像并从中复制构件。语法是:

dockerfile
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

使用先前的阶段作为新阶段

你可以通过在使用 FROM 指令时引用之前的阶段,从先前的阶段继续开始。例如:

dockerfile
# 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 中的 stage2 目标意味着只处理 basestage2。它不依赖于 stage1,所以会跳过该阶段。