多階段建置

對於任何曾努力最佳化 Dockerfile,同時又想保持其易讀性與維護性的人來說,多階段建構都非常實用。

使用多階段建置

透過多階段建構,您可以在 Dockerfile 中使用多個 FROM 陳述式。每個 FROM 指令都可以使用不同的基礎映像檔,且每個指令都會開啟一個新的建構階段。您可以選擇性地將組件從一個階段複製到另一個階段,並捨棄最終映像檔中不需要的一切。

下列 Dockerfile 包含兩個獨立的階段:一個用於建構二進位檔案,另一個則是將該檔案從第一個階段複製到下一個階段。

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

最終產出的是一個極小的生產環境映像檔,內部只有二進位檔案。應用程式建構所需的建構工具都不會被包含在最終映像檔中。

它是如何運作的?第二個 FROM 指令以 scratch 映像檔為基礎開啟了一個新的建構階段。COPY --from=0 行僅將前一階段已建構的組件複製到此新階段中。Go SDK 及任何中間產生的組件都會被遺留下來,而不會儲存到最終映像檔中。

命名您的建構階段

預設情況下,這些階段沒有名稱,您需透過整數編號來參考它們,從第一個 FROM 指令的 0 開始。不過,您可以在 FROM 指令中加入 AS <NAME> 來命名您的階段。此範例改進了先前的版本,為階段命名並在 COPY 指令中使用該名稱。這意味著即使日後調整了 Dockerfile 中指令的順序,COPY 也不會失效。

# syntax=docker/dockerfile:1
FROM golang:1.24 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 階段來載入測試資料,但使用不同的階段來建構用於生產環境的真實資料映像檔

使用外部映像檔作為一個階段

使用多階段建構時,您不限於複製在 Dockerfile 中較早建立的階段。您可以使用 COPY --from 指令從獨立的映像檔複製,可以使用本機映像檔名稱、本機或 Docker Registry 上可用的標籤 (tag),或是標籤 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

即使 stage2 不依賴它,傳統建構器仍會處理 stage1

© . This site is unofficial and not affiliated with Kubernetes or Docker Inc.