Docker下如何實現映象多階級構建?
Docker下如何實現映象多階級構建?
在Docker早期版本中,對於編譯型語言(例如C、Java、Go)的映象構建,我們只能將應用的編譯和執行環境的準備,全部放在一個Dockerfile
裡面,這就導致我們構建出來的映象體積很大,從而增加了映象的儲存和分發成本。
1、藉助額外指令碼構建
為了減小映象體積,我們需要藉助一個額外的指令碼,將映象的編譯過程和執行過程分開。
- 編譯階段:負責將我們的程式碼編譯成可執行的二進位制檔案。
- 執行時構建節點:準備應用程式執行的依賴環境,然後將編譯好的可執行物件拷貝到映象中。
以Go開發的一個HTTP服務為例,程式碼如下:
package main import ( "fmt" "io" "net/http" ) func getInfoHandler(w http.ResponseWriter, r *http.Request){ user := r.URL.Query().Get("user") if user != "" { io.WriteString(w,fmt.Sprintf("hello [%s]\n",user)) }else{ io.WriteString(w,"hello [Stranger]\n") } for k, v := range r.Header{ io.WriteString(w,fmt.Sprintf("%s=%s",k,v)) } } func health(w http.ResponseWriter, r *http.Request){ fmt.Fprintln(w,http.StatusOK) } func main() { http.HandleFunc("/",getInfoHandler) http.HandleFunc("/health",health) http.ListenAndServe(":8080",nil) }
將這個 Go 服務構建成映象分為兩個階段:程式碼的編譯階段和映象構建階段。
我們構建映象時,映象中需要包含 Go 語言編譯環境,應用的編譯階段我們可以使用 Dockerfile.build
檔案來構建映象。Dockerfile.build
的內容如下:
FROM golang:1.15
WORKDIR /go/src/httpServer/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
Dockefile.Build
可以幫助我們把程式碼編譯成可執行的二進位制檔案,我們使用以下的Dockerfile
構建一個執行環境:
FROM alpine:latest
WORKDIR /root/
COPY http-server .
CMD ["./http-server"]
我們將應用的編譯和執行環境的準備步驟,都放到一個 build.sh
指令碼檔案中,內容如下:
#!/bin/sh echo Building http-server:build # 宣告shell檔案,然後輸出開始構建資訊 # 使用 Dockerfile.build 檔案來構建一個臨時映象 http-server:build docker build -t http-server:build . -f Dockerfile.build # 用 http-server:build 映象建立一個名稱為 builder 的容器 # 該容器包含編譯後的 http-server 二進位制檔案。 docker create --name builder http-server:build # 使用docker cp命令從 builder 容器中拷貝 http-server 檔案到當前構建目錄 # 並且刪除名稱為 builder 的臨時容器。 docker cp builder:/go/src/httpServer/http-server ./http-server docker rm -f builder # 輸出開始構建映象資訊。 echo Building http-server:latest # 構建執行時映象,然後刪除臨時檔案 http-server docker build -t http-server:latest . rm ./http-server
上面我們使用 Dockerfile.build
檔案來編譯應用程式,使用 Dockerfile
檔案來構建應用的執行環境。然後我們通過建立一個臨時容器,把編譯後的 http-server
檔案拷貝到當前構建目錄中,然後再把這個檔案拷貝到執行環境的映象中,最後指定容器的啟動命令為 http-server。
使用這種方式雖然可以實現分離映象的編譯和執行環境,但是我們需要額外引入一個 build.sh 指令碼檔案,而且構建過程中,還需要建立臨時容器 builder 拷貝編譯後的 http-server 檔案,這使得整個構建過程比較複雜,並且整個構建過程也不夠透明。
為了解決這種問題, Docker 在 17.05 推出了多階段構建(multistage-build)的解決方案。
2、使用多階段構建
Docker 允許我們在 Dockerfile
中使用多個FROM
語句,而每個 FROM
語句都可以使用不同基礎映象。最終生成的映象,是以最後一條 FROM
為準,所以我們可以在一個 Dockerfile
中宣告多個 FROM
,然後選擇性地將一個階段生成的檔案拷貝到另外一個階段中,從而實現最終的映象只保留我們需要的環境和檔案。多階段構建的主要使用場景是分離編譯環境和執行環境。
接下來我們使用多階段構建將上述未使用多階段構建的過程精簡:
# 編譯,生成http-server
FROM golang:1.15
WORKDIR /go/src/httpServer/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
# 構建執行時映象
FROM alpine:latest
WORKDIR /root/
# --from=0 表示從第一階段構建結果中拷貝檔案到當前構建階段
COPY --from=0 /go/src/httpServer/http-server .
CMD ["./http-server"]
構建映象:
docker build -t http-server:latest .
3、多階段構建的其他使用方式
3.1、為構建階段命名
預設情況下,每一個構建階段都沒有被命名,可以通過 FROM
指令出現的順序來引用這些構建階段,構建階段的序號是從 0 開始的。然而,為了提高 Dockerfile
的可讀性,我們需要為某些構建階段起一個名稱,這樣即便後面我們對Dockerfile
中的內容程序重新排序或者添加了新的構建階段,其他構建過程中的 COPY
指令也不需要修改。
對上面的Dockerfile
進行優化:
# 編譯,生成http-server
FROM golang:1.15 AS builder
WORKDIR /go/src/httpServer/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
# 構建執行時映象
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /go/src/httpServer/http-server .
CMD ["./http-server"]
我們在第一個構建階段,使用 AS 指令將這個階段命名為 builder。然後在第二個構建階段使用 --from=builder
指令,即可從第一個構建階段中拷貝檔案,使得 Dockerfile
更加清晰可讀。
3.2、停止在特定的構建階段
有時候,我們的構建階段非常複雜,我們想在程式碼編譯階段進行除錯,但是多階段構建預設構建 Dockerfile
的所有階段,為了減少每次除錯的構建時間,我們可以使用 target 引數來指定構建停止的階段。
例如,我只想在編譯階段除錯 Dockerfile
檔案,可以使用如下命令:
# docker build -t http-server:latest . --target builder
Sending build context to Docker daemon 6.656kB
Step 1/4 : FROM golang:1.15 AS builder
---> 40349a2425ef
Step 2/4 : WORKDIR /go/src/httpServer/
---> Using cache
---> 90a929dd37de
Step 3/4 : COPY main.go .
---> Using cache
---> fee18351e532
Step 4/4 : RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
---> Using cache
---> e7356e46c9c4
Successfully built e7356e46c9c4
Successfully tagged http-server:latest
3.3、使用現有映象作為構建階段
使用多階段構建時,不僅可以從 Dockerfile
中已經定義的階段中拷貝檔案,還可以使用COPY --from指令從一個指定的映象中拷貝檔案,指定的映象可以是本地已經存在的映象,也可以是遠端映象倉庫上的映象。
例如,我們從上面編譯環境映象中拷貝出編譯好的二進位制檔案
# 構建執行時映象
FROM alpine:latest
WORKDIR /root/
COPY --from=http-server:latest /go/src/httpServer/http-server .
CMD ["./http-server"]
使用下面命令進行構建:
# docker build -t http-server:v2.0 .
Sending build context to Docker daemon 7.68kB
Step 1/4 : FROM alpine:latest
---> c059bfaa849c
Step 2/4 : WORKDIR /root/
---> Running in bc1e94677d94
Removing intermediate container bc1e94677d94
---> ab8143431ed0
Step 3/4 : COPY --from=http-server:latest /go/src/httpServer/http-server .
---> 133f25c0d3f0
Step 4/4 : CMD ["./http-server"]
---> Running in d625e202db78
Removing intermediate container d625e202db78
---> 00a4c8370bc9
Successfully built 00a4c8370bc9
Successfully tagged http-server:v2.0
又例如,當我們想要拷貝 nginx 官方映象的配置檔案到我們自己的映象中時,可以在 Dockerfile 中使用以下指令:
COPY --from=nginx:latest /etc/nginx/nginx.conf /etc/local/nginx.conf