多階段建置
多階段建置對於努力最佳化 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
目標表示只會處理 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
舊版建置器會處理 stage1
,即使 stage2
不依賴它。