多階段構建是一個新特性,需要 Docker 17.05 或更高版本的守護進程和客戶端。對於那些努力優化 Dockerfiles 並使其易於閱讀和維護的人來說,多階段構建非常有用。
在多階段構建之前
構建鏡像時最具挑戰性的事情之一就是縮小鏡像大小。Dockerfile 中的每一條指令都會在鏡像中添加一個層,在進入下一層之前,您需要記住清除所有不需要的工件。要編寫一個真正高效的 Dockerfile,傳統上需要使用 shell 技巧和其他邏輯來保持層盡可能小,並確保每一層都有它需要的來自前一層的工件,而沒有其他東西。
實際上,有一個 Dockerfile 用於開發環境(包含構建應用程序所需的所有內容),同時有一個精簡的 Dockerfile 用於生產環境(僅包含應用程序和運行應用程序所需的內容)是非常常見的。這被稱為“建造者模式”。維護兩個 Dockerfiles 並不理想。
這里有一個例子 Dockerfile.build
文件以及符合上述建造者模式的 Dockerfile
:
Dockerfile.build
:
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
請注意,此示例還使用 Bash 操作符 &&
將兩個 RUN
命令人為壓縮在一起,以避免在鏡像中創建額外的層。這很容易發生故障,也很難維護。例如,很容易插入另一個命令而忘記使用 \
字符繼續行。
Dockerfile
:
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]
build.sh
:
#!/bin/sh
echo Building alexellis2/href-counter:build
docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \
-t alexellis2/href-counter:build . -f Dockerfile.build
docker container create --name extract alexellis2/href-counter:build
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app
docker container rm -f extract
echo Building alexellis2/href-counter:latest
docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app
當你運行 build.sh
腳本,它需要構建第一個鏡像,從中創建一個容器來復制工件,然后構建第二個鏡像。這兩個鏡像在您的系統上占用空間,並且您的本地磁盤上仍然有 app
工件。
多階段構建極大地簡化了這種情況!
使用多階段構建
對於多階段構建,可以在 Dockerfile 中使用多個 FROM
語句。每個 FROM
指令都可以使用不同的基鏡像,並且它們都開始了構建的新階段。您可以選擇性地將工件從一個階段復制到另一個階段,舍棄在最終鏡像中您不想要的所有內容。為了說明這是如何工作的,讓我們使用多階段構建調整前一節中的 Dockerfile。
Dockerfile
:
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
您只需要一個 Dockerfile。您也不需要單獨的構建腳本。只要運行 docker build
。
$ docker build -t alexellis2/href-counter:latest .
最終的結果是與前面相同的微小生產鏡像,並且顯著降低了復雜性。您不需要創建任何中間鏡像,也不需要將任何工件提取到本地系統中。
它是如何工作的?第二個 FROM
指令用 alpine:latest
鏡像作為基礎,開始一個新的構建階段。COPY --from=0
行只將前一階段的構建工件復制到這個新階段。Go SDK 和任何中間工件都會被留下,不會保存在最終的鏡像中。
為構建階段命名
默認情況下,沒有對階段進行命名,可以通過它們的整數來引用它們,FROM
指令的第一個整數從 0 開始。但是,您可以通過添加一個 AS <NAME>
到 FROM
指令來命名階段。下面示例通過命名階段並在 COPY
指令中使用名稱改進了前面一個示例。這意味着,即使 Dockerfile 中的指令稍后被重新排序,COPY
也不會破壞。
FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
在特定的構建階段停止
在構建映像時,不必構建包括每個階段的整個 Dockerfile。你可以指定目標構建階段。以下命令假設你正在使用之前的 Dockerfile
,但是在名為 builder
的階段停止:
$ docker build --target builder -t alexellis2/href-counter:latest .
這可能非常強有力的幾個場景是:
- 調試一個特定的構建階段
- 使用一個啟用了所有調試符號或工具的
調試(debug)
階段和一個精益的生產(production)
階段 - 使用一個
測試(testing)
階段,在這個階段你的應用會被測試數據填充,但是在構建產品時,使用一個使用真實數據的不同階段。
使用外部鏡像作為“階段”
當使用多階段構建時,您不受限於從 Dockerfile 中先前創建的階段進行復制。您可以使用 COPY --from
指令從單獨的鏡像中進行復制,可以使用本地鏡像名稱、本地或 Docker 注冊表上可用的標簽或標簽 ID。Docker 客戶端會在必要時拉取鏡像並從中復制工件。語法是:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
把以前的階段作為新的階段
在使用 FROM
指令時,您可以引用前一階段的內容。例如:
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