1. 程式人生 > 其它 >Docker下如何實現映象多階級構建?

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