Docker:使用多階段構建映象
多階段構建是 Docker 17.05 及更高版本提供的新功能。這對致力於優化 Dockerfile 的人來說,使得 Dockerfile 易於閱讀和維護。
致謝: 特別感謝 Alex Ellis 授權使用他的關於 Docker 多階段構建的部落格文章 Builder pattern vs. Multi-stage builds in Docker 作為以下示例的基礎。
在多階段構建之前
關於構建映象最具挑戰性的事情之一是保持映象體積小巧。 Dockerfile 中的每條指令都會在映象中增加一層,並且在移動到下一層之前,需要記住清除不需要的構件。要編寫一個非常高效的 Dockerfile,你通常需要使用 shell 技巧和其它方式來儘可能地減少層數,並確保每一層都具有上一層所需的構件,而其它任何東西都不需要。
實際上最常見的是,有一個 Dockerfile 用於開發(其中包含構建應用程式所需的所有內容),而另一個裁剪過的用於生產環境,它只包含您的應用程式以及執行它所需的內容。這被稱為“構建器模式”。但是維護兩個 Dockerfile 並不理想。
下面分別是一個 Dockerfile.build 和遵循上面的構建器模式的 Dockerfile 的例子:
Dockerfile.build:
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 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 create --name extract alexellis2/href-counter:build docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app docker 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 指令可以使用不同的基礎映象,並且每次 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 開始。但是,你可以通過在 FROM 指令中使用 as來為階段命名。以下示例通過命名階段並在 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"]