一、導讀
本文主要介紹在編寫 docker 鏡像的時候一些需要注意的事項和推薦的做法。
雖然 Dockerfile 簡化了鏡像構建的過程,并且把這個過程可以進行版本控制,但是不正當的
Dockerfile 使用也會導致很多問題。
- docker 鏡像太大。如果你經常使用鏡像或者構建鏡像,一定會遇到那種很大的鏡像,甚至有些能達到 2G 以上
- docker 鏡像的構建時間過長。每個 build 都會耗費很長時間,對于需要經常構建鏡像(比如單元測試)的地方這可能是個大問題
- 重復勞動。多次鏡像構建之間大部分內容都是完全一樣而且重復的,但是每次都要做一遍,浪費時間和資源
希望讀者能夠對 docker 鏡像有一定的了解,閱讀這篇文章至少需要一下前提知識
- 了解 docker 的基礎概念,運行過容器
- 熟悉 docker 鏡像的基礎知識,知道鏡像的分層結構
- 最好是負責過某個 docker 鏡像的構建(使用 docker build 命令創建過自己的鏡像)
- Dockerfile 和鏡像構建
Dockerfile 是由一個個指令組成的,每個指令都對應著最終鏡像的一層。每行的第一個單詞就是命令,后面所有的字符串是這個命令的參數,關于 Dockerfile 支持的命令以及它們的用法,可以參考官方文檔,這里不再贅述。
當運行 docker build 命令的時候,整個的構建過程是這樣的:
a. 讀取 Dockerfile 文件發送到 docker daemon
b. 讀取當前目錄的所有文件(context),發送到 docker daemon
c. 對 Dockerfile 進行解析,處理成命令加上對應參數的結構
d. 按照順序循環遍歷所有的命令,對每個命令調用對應的處理函數進行處理
e. 每個命令(除了 FROM)都會在一個容器執行,執行的結果會生成一個新的鏡像,為最后生成的鏡像打上標簽
二、編寫 Dockerfile 的一些最佳實踐
1.使用統一的 base 鏡像
有些文章講優化鏡像會提倡使用盡量小的基礎鏡像,目前集團操作系統一級提供統一的基礎鏡像,一些BU也根據自己的技術規范定義了BU級的基礎鏡像,一般的應用只需要FROM自己BU提供的基礎鏡像即可,因為基礎鏡像只需要下載一次可以共享,并不會造成太多的存儲空間浪費。它的好處是這些鏡像的生態比較完整,方便我們安裝軟件,除了問題方便調試。
2.動靜分離
經常變化的內容和基本不會變化的內容要分開,把不怎么變化的內容放在下層,創建出來不同基礎鏡像供上層使用。比如可以創建各種語言的基礎鏡像,這些鏡像包含了最基本的語言庫,每個組可以在上面繼續構建應用級別的鏡像。
3.最小原則:只安裝必需的東西
很多人構建鏡像的時候,都有一種沖動——把可能用到的東西都打包到鏡像中。要遏制這種想法,鏡像中應該只包含必需的東西,任何可以有也可以沒有的東西都不要放到里面。因為鏡像的擴展很容易,而且運行容器的時候也很方便地對其進行修改。這樣可以保證鏡像盡可能小,構建的時候盡可能快,也保證未來的更快傳輸、更省網絡資源。
4.一個原則:每個鏡像只有一個功能
不要在容器里運行多個不同功能的進程,每個鏡像中只安裝一個應用的軟件包和文件,需要交互的程序通過 容器之間的網絡進行交流。這樣可以保證模塊化,不同的應用可以分開維護和升級,也能減小單個鏡像的大小。
5.使用更少的層
雖然看起來把不同的命令盡量分開來,寫在多個命令中容易閱讀和理解。但是這樣會導致出現太多的鏡像層,而不好管理和分析鏡像,而且鏡像的層是有限的。盡量把相關的內容放到同一個層,使用換行符進行分割,這樣可以進一步減小鏡像大小,并且方便查看鏡像歷史。
6.減少每層的內容
盡管只安裝必須的內容,在這個過程中也可能會產生額外的內容或者臨時文件,我們要盡量讓每層安裝的東西保持最小。
比如使用 --no-install-recommends 參數告訴 apt-get 不要安裝推薦的軟件包
7.不要在 Dockerfile 中單獨修改文件的權限
因為 docker 鏡像是分層的,任何修改都會新增一個層,修改文件或者目錄權限也是如此。如果有一個命令單獨修改大文件或者目錄的權限,會把這些文件復制一份,這樣很容易導致鏡像很大。
解決方案也很簡單,要么在添加到 Dockerfile 之前就把文件的權限和用戶設置好,要么在容器啟動腳本(entrypoint)做這些修改,或者拷貝文件和修改權限放在一起做(這樣最終也只是增加一層)。
8.利用 cache 來加快構建速度
如果 Docker 發現某個層已經存在了,它會直接使用已經存在的層,而不會重新運行一次。如果你連續運行 docker build 多次,會發現第二次運行很快就結束了。
不過從 1.10 版本開始,Content Addressable Storage 的引入導致緩存功能的實效,目前引入了 --cache-from 參數可以手動指定一個鏡像來使用它的緩存。
反之,如果想要緩存失效(禁用此層及后面層的緩存),可以在對應層前加LABEL語句:
LABEL key=value ,每次修改value的值,使得緩存失效。
9.版本控制和自動構建
最好把 Dockerfile 和對應的應用代碼一起放到版本控制中,然后能夠自動構建鏡像。這樣的好處是可以追蹤各個版本鏡像的內容,方便了解不同鏡像有什么區別,對于調試和回滾都有好處。
另外,如果運行鏡像的參數或者環境變量很多,也要有對應的文檔給予說明,并且文檔要隨著 Dockerfile 變化而更新,這樣任何人都能參考著文檔很容易地使用鏡像,而不是下載了鏡像不知道怎么用。
10.使用一個.dockerignore文件
在大部分情況下,最好的做法是將每一個Dockerfile文件放到一個空的文件夾里。接著,把構建Dockerfile所需的文件添加到這個文件下。為了提高構建的效率,你可以在這個文夾下添加一個.dockerignore 文件來排除那些沒用的文件和文件夾。這個文件支持類似 .gitignore 文件那樣的排除模式。關于如何創建它,可以移步到dockerignore 文件。