1. 程式人生 > 其它 >寫DockerFile的一些技巧

寫DockerFile的一些技巧

Docker映象由只讀層組成,每個層都代表一個Dockerfile指令。這些層是堆疊的,每一層都是前一層變化的增量。示例Dockerfile:

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

每條指令建立一個層:
FROM從ubuntu:15.04Docker映象建立一個圖層。
COPY 從Docker客戶端的當前目錄新增檔案。
RUN用你的應用程式構建make。
CMD 指定在容器中執行的命令。

執行影象並生成容器時,可以 在基礎圖層的頂部新增新的_可寫層_(“容器圖層”)。對正在執行的容器所做的所有更改(例如寫入新檔案,修改現有檔案和刪除檔案)都將寫入此可寫容器層。

使用標籤

給映象打上標籤, 易讀的映象標籤可以幫助瞭解映象的功能。

使用統一的Base映象

比如busybox或者alpine,謹慎選擇基礎映象,儘量選擇當前官方的映象庫中映象;
很多教程中建議大家使用alpine映象,更建議大家使用centos,Ubuntu這樣的映象。同時,在構建自己的Docker映象時,只安裝和更新必須使用的包,FROM指令應該包含的引數tag,比如使用centos:7.5.1504而不是FROM centos。

充分利用快取

在映象的構建過程中,Docker 會遍歷 Dockerfile 檔案中的指令,然後按順序執行。在執行每條指令之前,Docker 都會在快取中查詢是否已經存在可重用的映象,如果有就使用現存的映象,不再重複建立。如果你不想在構建過程中使用快取,你可以在 docker build 命令中使用 --no-cache=true 選項;
但是,如果你想在構建的過程中使用快取,你得明白什麼時候會,什麼時候不會找到匹配的映象,遵循的基本規則如下:

  • 從一個基礎映象開始(FROM 指令指定),下一條指令將和該基礎映象的所有子映象進行匹配,檢查這些子映象被建立時使用的指令是否和被檢查的指令完全一樣。如果不是,則快取失效。
  • 在大多數情況下,只需要簡單地對比 Dockerfile 中的指令和子映象。然而,有些指令需要更多的檢查和解釋。
  • 對於 ADD 和 COPY 指令,映象中對應檔案的內容也會被檢查,每個檔案都會計算出一個校驗和。檔案的最後修改時間和最後訪問時間不會納入校驗。在快取的查詢過程中,會將這些校驗和和已存在映象中的檔案校驗和進行對比。如果檔案有任何改變,比如內容和元資料,則快取失效。
  • 除了 ADD 和 COPY 指令,快取匹配過程不會檢視臨時容器中的檔案來決定快取是否匹配。例如,當執行完 RUN apt-get -y update 指令後,容器中一些檔案被更新,但 Docker 不會檢查這些檔案。這種情況下,只有指令字串本身被用來匹配快取。

一旦快取失效,所有後續的 Dockerfile 指令都將產生新的映象,快取不會被使用。

正確使用ADD和COPY指令

這兩者很相似,推薦有限選擇 COPY,它比 ADD 透明度更高。

  • COPY,只支援將本地檔案複製到容器中
  • ADD,除了 COPY 的功能外,還支援遠端 URL。但最好的用途是將本地 tar 檔案提取到映象中 ADD rootfs.tar.xz /。

如果在 Dockerfile 中使用不用的檔案,那麼 COPY 它們可以單獨使用。這樣,特定檔案的更改,將確保每一步的構建快取無效, 如:

DOCKERFILECOPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

將 COPY . /tmp/ 放在後面,這能夠使 RUN 的快取無效的數量減少。儘量使用docker volume共享檔案,而不是用ADD指令新增檔案;

不要在Dockerfile中單獨修改檔案許可權

因為 docker 映象是分層的,任何修改都會新增一個層,修改檔案或者目錄許可權也是如此。如果有一個命令單獨修改大檔案或者目錄的許可權,會把這些檔案複製一份,這樣很容易導致映象很大;

解決方案也很簡單,要麼在新增到 Dockerfile 之前就把檔案的許可權和使用者設定好,要麼在容器啟動指令碼(entrypoint)做這些修改,或者拷貝檔案和修改許可權放在一起做(這樣最終也只是增加一層;

版本控制和自動構建

最好把 Dockerfile 和對應的應用程式碼一起放到版本控制中,然後能夠自動構建映象。這樣的好處是可以追蹤各個版本映象的內容,方便了解不同映象有什麼區別,對於除錯和回滾都有好處。

另外,如果執行映象的引數或者環境變數很多,也要有對應的文件給予說明,並且文件要隨著 Dockerfile 變化而更新,這樣任何人都能參考著文件很容易地使用映象,而不是下載了映象不知道怎麼用。

RUN指令

為了使Dockerfile易讀、易理解和可維護,在使用比較長的RUN指令是可以使用反斜槓\分隔多行。將多行引數按字母順序排序(比如要安裝多個包時)。這可以幫助你避免重複包含同一個包,更新包列表時也更容易。也便於 PRs 閱讀和審查。建議在反斜槓符號 \ 之前新增一個空格,以增加可讀性。

RUN yum update && yum install -y \
  vim \
  ntpdate \
  git \
  nginx

CMD和ENTRYPOINT指令

CMD和ENTRYPOINT指令指定了容器執行的預設命令,推薦二者結合使用。使用exec格式ENTRYPOINT指令設定固定的預設命令和引數,然後使用CMD指令設定可變的引數。

不要在Dockerfile中做埠對映

Docker的兩個核心概念是可重複性和可移植性,映象應該可以在任何主機上執行多次。對映埠會破壞映象的可移植性,且這樣的映象只能在一臺主機上啟動一個容器。所以埠對映應在docker run命令中用-p引數指定。

# 不要在Dockerfile中做如下對映
EXPOSE 80:8080
# 僅僅暴露80埠,需要另做對映
EXPOSE 80

使用多階段構建

在 Docker 17.05 以上版本中,你可以使用 多階段構建 來減少所構建映象的大小;

避免安裝不必要的包

為了降低複雜性、減少依賴、減小檔案大小、節約構建時間,你應該避免安裝任何不必要的包。例如,不要在資料庫映象中包含一個文字編輯器。

一個容器只執行一個程序

應該保證在一個容器中只執行一個程序。將多個應用解耦到不同容器中,保證了容器的橫向擴充套件和複用。例如 web 應用應該包含三個容器: web應用.資料庫,快取;
如果容器互相依賴,你可以使用 Docker 自定義網路 來把這些容器連線起來。

映象層數儘可能少

你需要在 Dockerfile 可讀性(也包括長期的可維護性)和減少層數之間做一個平衡;

用python -m pip而不是pip

這是為了確保我們使用的 pip 是我們想用的那個 python 對應的 pip。有時候,一個系統裡安裝了 Python 2 和 Python 3,而我們可能錯誤地設定了 PATH 環境變數(或則因為其他的原因),導致我們執行 python 命令的時候,啟動的 Python 3(或者 2),但是 pip 命令是 Python 2(或者3)的 pip。還有一些其他原因使我們更應該用 python -m pip 的,詳見 https://snarky.ca/why-you-should-use-python-m-pip/

一個典型的例子(升級 pip)
python -m pip install --quiet --upgrade pip

讓pip install 更安靜

上例中,在 pip install 命令裡,我們用了 --quiet 引數,減少 pip install 打印出來的資訊。這樣可以讓 docker build 更安靜。尤其是,如果在 CI 裡執行 docker build 的話,減少列印資訊可以讓 CI log 更加可讀。

讓apt-get install 更安靜

類似的,用 apt-get 安裝軟體包的時候,我們用 -qq 命令,甚至重定向輸出到 /dev/null 讓它更安靜。

apt-get -qq update
apt-get -qq install -y curl > /dev/null

讓curl和wget更安靜

首先,如果要下載檔案,curl 和 wget 二選一即可。如果用 curl,可以用 --silent 引數

curl -sLO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64

wget 有 --quiet 引數

wget -q https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64

用axel而不是curl或wget

作為一個開源軟體,中外開發者都會執行 docker build。開發者所處的地理位置不同,各自都希望從距離自己最近(最快)的 mirror 下載和安裝檔案。axel 可以從多個 mirror 下載同一個檔案,根據各個 mirror 的速度,決定分別從不同 mirror 下載的位元組數量。如果有的 mirror 掛了,axel 可以忽略之。尤其對於身處國內的開發者,axel 完全可以取代 curl 以及 wget;

axel 和 wget 一樣支援 --quiet 引數。以下是一個從大洋兩岸的 mirrors 下載 Go 編譯器的例子;

echo "Install Go compiler ..."
GO_MIRROR_0="http://mirrors.ustc.edu.cn/golang/go1.13.4.linux-amd64.tar.gz"
GO_MIRROR_1="https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz"
axel --quiet --output go.tar.gz $GO_MIRROR_0 $GO_MIRROR_1`````````````````````

讓python setup.py更安靜

有時候我們會在 Dockerfile 裡 build 和 install Python packages,此時我們需要執行

python ./setup.py build --quiet
python ./setup.py install --quiet

不過如果我們要 build binary distribution package,則需要注意使用全域性引數 --quiet

python ./setup.py --quiet bdist_wheel

明辨ARG和ENV

ARG 和 ENV 是 Dockerfile 裡用來定製化 Docker image 的利器,經常結合在一起使用,也常領 Dockerfile 新手撓頭。其實,記住一下幾條規則,基本就可以了;

  1. ARG 存在於 docker build 命令執行期間。預設值寫在 Dockerfile 裡。如果需要修改,可以通過 docker build 命令裡的 --build-arg 引數來指定。
  2. ENV 存在於 docker run 命令執行期間。預設值寫在 Dockerfile 裡。如果要修改,可以通過 docker run 命令的 --env 引數來指定。
  3. 如果要把 ARG 的值儲存到 container 執行起來之後仍然可以可用,則需要在 ARG 之後寫一個 ENV。

為了方便理解,請看下面幾個例子。第一個例子:為了把 ARG 的值儲存到 docker run 的時候也可以被用到,我們把它寫入一個檔案 /root/hello.sh;

FROM ubuntu:18.04
ARG releaser=youmen
RUN echo "echo $releaser" > /root/hello.sh
RUN chmod +x /root/hello.sh

這樣,我們可以 docker run 的時候執行 /root/hello.sh,打印出 docker bulid 的時候指定的 releaser;

docker build -t dev .
docker run --rm -it dev bash -c /root/hello.sh # 打印出 youmen

不過因為 ARG 只存在於 docker build 命令執行期間,所以下面命令什麼也打印不出來

docker run --rm -it dev bash -c "echo $releaser"

如果要讓上面命令也可以打印出 releaser 這個 ARG 的值,可以在 Dockerfile 里加一個 ENV;

FROM ubuntu:18.04
ARG releaser=王益
ENV releaser=$releaser

這樣,下面命令就也可以打印出”王益“了;

docker build -t dev .
docker run --rm -it dev bash -c "echo $releaser"

docker build --quiet

上面一些經驗是讓 docker build 變得更安靜的。如果要極端的安靜,不需要通過在寫 Dockerfile 的時候注意什麼,只需要在 docker build 命令里加上 --quiet