1. 程式人生 > >編寫 Dockerfile 的五個最佳實踐

編寫 Dockerfile 的五個最佳實踐

此文適合Docker初學入門讀者,大師請繞行!,遵守最佳實踐可少踩坑、提升效能體驗及可移植性,期望對讀者有所幫助!

什麼是Dockerfile

Dockerfile 是一個文字檔案,裡面包含了打包Docker映象所需要用到的命令。Docker 可以通過讀取 Dockerfile 裡面的命令來自動化地構建Docker映象。通過執行 docker build 就可以啟動這樣的一個自動化流程。

$ docker build -f Dockerfile .
Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM nginx:latest
 ---> f895b3fb9e30
Successfully built f895b3fb9e30

容器映象層

Docker映象由只讀層組成,每個層都代表一個Dockerfile指令,這些層是堆疊的,每一層都是前一層變化的增量。執行映象並生成容器時,可以在基礎映象的頂部新增新的可寫層(“容器層”)。對正在執行的容器所做的所有更改(例如寫入新檔案,修改現有檔案和刪除檔案)都將寫入此可寫容器層。

這裡寫圖片描述

01 瞭解Docker Build上下文(Context)

Docker build有一個重要引數context,預設是當前目錄,採用.代替,但為了避免將不必要的檔案打包到映象,造成context及映象大小過大,也可以用指定的目錄作為context。context過大會造成docker build很耗時,映象過大則會造成docker pull/push效能變差以及執行時容器體積過大浪費空間資源。

對於Docker17.05及以上版本,有一個remote context的特性,即可以將遠端資源作為context,如下所示:

$ docker build -t nginx:v1 https://gitlab.com/fuhui/docker-lab.git
Sending build context to Docker daemon  50.18kB
Step 1/1 : FROM nginx:latest
 ---> f895b3fb9e30
Successfully built f895b3fb9e30
Successfully tagged nginx:v1

02 分階段構建

構建映象最大的挑戰莫過於防止映象過大,造成實際執行時由於併發而導致拉取效能問題。為了應對這個挑戰,很多轉型到容器的團隊採用兩個Dockerfile,一個負責開發環境的映象構建,一個負責生產環境的映象構建。開發映象包含了程式碼構建所需要的環境,映象大小自然比較大,生產映象僅包含應用執行所需要的內容,是很精簡的體積很小的映象。

一個開發映象Dockerfile示例如下:

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 .

一個生產映象Dockerfile示例如下:

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"] 

這時候整個過程分為三步:1.根據開發映象啟動一個容器來構建app 2. 構建結束後的app二進位制拷貝到主機 3. 根據生產映象啟動一個容器,將主機上的app二進位制copy到生產映象中。如下指令碼可自動化執行該過程。

#!/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

這麼做會大量佔用主機的空間資源,並且執行一段時間後,遺留很多二進位制app檔案在主機上,後期清理還需要額外的維護工作,顯然不是一個很好的實踐。

Docker分階段構建就可以很好地解決這個問題,使用如下的Dockerfile即可:

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流程很清晰,也不在需要額外的shell指令碼來支援整個流程,並且我們可以指定執行的stage,具體命令如下:

$ docker build --target builder -t alexellis2/href-counter:latest .

03 使用構建快取

構建映像時,Docker會逐步按指定的順序執行Dockerfile中的每個指令。在檢查每條指令時,Docker會在其快取中查詢可以重用的現有映象,而不是重複建立新的映象。

如果不想使用快取,可以在docker build時加上--no-cache=true引數。然後,使用快取會使得整個構建過程更高效,因此瞭解什麼樣的情形下可以利用快取來提升效率很重要。具體來說有下面幾個情形需要了解:

  • 從已經在快取中的父映象開始,將下一條指令與從該基本映象匯出的所有子映象進行比較,以檢視它們中的一個是否使用完全相同的指令構建。如果不是,則快取無效。

  • 在大多數情況下,只需將Dockerfile中的指令與其中一個子映象進行比較即可。但是,某些Dockerfile命令需要更深入的檢查。對於ADD和COPY指令,將檢查映象中檔案的內容,併為每個檔案計算校驗和。在這些校驗和中不考慮檔案的最後修改時間和最後訪問時間。在快取查詢期間,將校驗和與現有映像中的校驗和進行比較。如果檔案中有任何更改(例如內容和元資料),則快取無效。

  • 除了ADD和COPY命令之外,快取檢查不會檢視容器中的檔案以確定快取匹配。例如,在處理RUN apt-get -y update命令時,不檢查容器中更新的檔案以確定是否存在快取命中。在這種情況下,只需使用命令字串本身來查詢匹配項。

04 給映象設定標籤(Label)

每一個物件都包含相應的元資料資訊,比如Docker映象就包含作者、大小等等元資料資訊。在使用映象的時候,往往會通過這些元資料資訊來查詢適合的映象來用於開發或測試,而不單單只是通過名字去檢索。

在Dockerfile中可以使用Label命令來為映象增加Label,示例如下:

FROM nginx:latest
LABEL version=2.0

我們可以檢視到Label及使用Label進行篩選:

$ docker build -t nginx:2.0 https://gitlab.com/fuhui/docker-lab.git
Sending build context to Docker daemon  50.18kB
Step 1/2 : FROM nginx:latest
 ---> f895b3fb9e30
Step 2/2 : LABEL version 2.0
 ---> Running in 721d056eec21
 ---> 5af7bc144cb6
Removing intermediate container 721d056eec21
Successfully built 5af7bc144cb6
Successfully tagged nginx:2.0
$ docker inspect 5af7bc144cb6
...
    "Labels": {
        "maintainer": "NGINX Docker Maintainers <[email protected]>",
        "version": "2.0"
    },
...
$ docker images --filter "label=version=2.0"
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               2.0                 5af7bc144cb6        3 minutes ago       108MB

05 CMD和ENTRYPOINT指令結合

官方關於CMD和ENTRYPOINT指令的說明如下

CMD
The main purpose of a CMD is to provide defaults for an executing container.
ENTRYPOINT
An ENTRYPOINT helps you to configure a container that you can run as an executable.

簡而言之,就是CMD提供執行時的動態覆蓋引數機制,而ENTRYPOINT只是容器啟動時的執行入口。

假設有如下Dockerfile:

FROM debian
ENTRYPOINT ["/bin/ping"]
CMD ["localhost"]

當不指定任何引數時,情形如下:

$ docker run -it ping-test
PING localhost (127.0.0.1): 48 data bytes
56 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.096 ms
56 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.088 ms
56 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.088 ms

當指定一個ip地址,如192.168.137.4時,情況如下:

$ docker run -it ping-test 192.168.137.4
56 bytes from 192.168.137.4: icmp_seq=0 ttl=55 time=32.583 ms
56 bytes from 192.168.137.4: icmp_seq=2 ttl=55 time=30.327 ms
56 bytes from 192.168.137.4: icmp_seq=4 ttl=55 time=46.379 ms

這種機制意味著更好的可移植性,使用者可在執行時動態注入變數,如當前環境型別、認證資訊等等,便於服務的遷移、擴容等場景。

總結

Dockerfile 在實際基於docker的開發中使用非常普遍,很多開發者都掌握了基本的編寫技巧,但對於一些優化的策略、方法掌握比較少,那麼以上這些實踐能夠幫助大家節省時間、提升效率和效能,甚至提升應用的移植靈活性等。