多階段建置
對於任何曾努力最佳化 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 目標意味著只會處理 base 和 stage2。由於對 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。