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