多階段建置

多階段建置對於努力最佳化 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 .

最終結果是一個只包含二進位檔的小型 production 映像檔。建置應用程式所需的建置工具都不包含在產生的映像檔中。

它是如何運作的?第二個 FROM 指令使用 scratch 映像檔作為其基底,開始一個新的建置階段。COPY --from=0 行僅將建置的構件從前一個階段複製到這個新階段。Go SDK 和任何中間構件都將被遺棄,並且不會儲存在最終映像檔中。

命名您的建置階段

根據預設,階段沒有命名,您可以使用它們的整數編號來參考它們,從第一個 FROM 指令的 0 開始。但是,您可以通過在 FROM 指令中新增 AS <名稱> 來命名您的階段。此範例通過命名階段並在 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 階段,但使用使用真實資料的不同階段建置 production

使用外部映像檔作為階段

使用多階段建置時,您不僅限於從您在 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 不依賴它。