原文連結:你確定你會寫 Dockerfile 嗎?
如今 GitHub 倉庫中已經包含了成千上萬的 Dockerfile,但並不是所有的 Dockerfile 都是高效的。本文將從五個方面來介紹 Dockerfile 的最佳實踐,以此來幫助大家編寫更優雅的 Dockerfile。如果你是 Docker 的初學者,恭喜你,這篇文章就是為你準備的。後面的系列將會更加深入,敬請期待!
本文使用一個基於
Maven
的 Java 專案作為示例,然後不斷改進 Dockerfile 的寫法,直到最後寫出一個最優雅的 Dockerfile。中間的所有步驟都是為了說明某一方面的最佳實踐。
1. 減少構建時間
一個開發週期包括構建 Docker 映象,更改程式碼,然後重新構建 Docker 映象。在構建映象的過程中,如果能夠利用快取,可以減少不必要的重複構建步驟。
構建順序影響快取的利用率
映象的構建順序很重要,當你向 Dockerfile 中新增檔案,或者修改其中的某一行時,那一部分的快取就會失效,該快取的後續步驟都會中斷,需要重新構建。所以優化快取的最佳方法是把不需要經常更改的行放到最前面,更改最頻繁的行放到最後面。
只拷貝需要的檔案,防止快取溢位
當拷貝檔案到映象中時,儘量只拷貝需要的檔案,切忌使用 COPY .
指令拷貝整個目錄。如果被拷貝的檔案內容發生了更改,快取就會被破壞。在上面的示例中,映象中只需要構建好的 jar 包,因此只需要拷貝這個檔案就行了,這樣即使其他不相關的檔案發生了更改也不會影響快取。
最小化可快取的執行層
每一個 RUN
指令都會被看作是可快取的執行單元。太多的 RUN 指令會增加映象的層數,增大映象體積,而將所有的命令都放到同一個 RUN 指令中又會破壞快取,從而延緩開發週期。當使用包管理器安裝軟體時,一般都會先更新軟體索引資訊,然後再安裝軟體。推薦將更新索引和安裝軟體放在同一個 RUN 指令中,這樣可以形成一個可快取的執行單元,否則你可能會安裝舊的軟體包。
2. 減小映象體積
映象的體積很重要,因為映象越小,部署的速度更快,攻擊範圍越小。
刪除不必要依賴
刪除不必要的依賴,不要安裝除錯工具。如果實在需要除錯工具,可以在容器執行之後再安裝。某些包管理工具(如 apt
)除了安裝使用者指定的包之外,還會安裝推薦的包,這會無緣無故增加映象的體積。apt 可以通過新增引數 -–no-install-recommends
來確保不會安裝不需要的依賴項。如果確實需要某些依賴項,請在後面手動新增。
刪除包管理工具的快取
包管理工具會維護自己的快取,這些快取會保留在映象檔案中,推薦的處理方法是在每一個 RUN 指令的末尾刪除快取。如果你在下一條指令中刪除快取,不會減小映象的體積。
當然了,還有其他更高階的方法可以用來減小映象體積,如下文將會介紹的多階段構建。接下來我們將探討如何優化 Dockerfile 的可維護性、安全性和可重複性。
3. 可維護性
儘量使用官方映象
使用官方映象可以節省大量的維護時間,因為官方映象的所有安裝步驟都使用了最佳實踐。如果你有多個專案,可以共享這些映象層,因為他們都可以使用相同的基礎映象。
使用更具體的標籤
基礎映象儘量不要使用 latest
標籤。雖然這很方便,但隨著時間的推移,latest 映象可能會發生重大變化。因此在 Dockerfile 中最好指定基礎映象的具體標籤。我們使用 openjdk
作為示例,指定標籤為 8。其他更多標籤請檢視官方倉庫。
使用體積最小的基礎映象
基礎映象的標籤風格不同,映象體積就會不同。slim
風格的映象是基於 Debian 發行版製作的,而 alpine
風格的映象是基於體積更小的 Alpine Linux 發行版製作的。其中一個明顯的區別是:Debian 使用的是 GNU 專案所實現的 C 語言標準庫,而 Alpine 使用的是 Musl C 標準庫,它被設計用來替代 GNU C 標準庫(glibc)的替代品,用於嵌入式作業系統和移動裝置。因此使用 Alpine 在某些情況下會遇到相容性問題。 以 openjdk 為例,jre
風格的映象只包含 Java 執行時,不包含 SDK
,這麼做也可以大大減少映象體積。
4. 重複利用
到目前為止,我們一直都在假設你的 jar 包是在主機上構建的,這還不是理想方案,因為沒有充分利用容器提供的一致性環境。例如,如果你的 Java 應用依賴於某一個特定的作業系統的庫,就可能會出現問題,因為環境不一致(具體取決於構建 jar 包的機器)。
在一致的環境中從原始碼構建
原始碼是你構建 Docker 映象的最終來源,Dockerfile 裡面只提供了構建步驟。
首先應該確定構建應用所需的所有依賴,本文的示例 Java 應用很簡單,只需要 Maven
和 JDK
,所以基礎映象應該選擇官方的體積最小的 maven 映象,該映象也包含了 JDK。如果你需要安裝更多依賴,可以在 RUN 指令中新增。pom.xml
檔案和 src
資料夾需要被複制到映象中,因為最後執行 mvn package
命令(-e 引數用來顯示錯誤,-B 參數列示以非互動式的“批處理”模式執行)打包的時候會用到這些依賴檔案。
雖然現在我們解決了環境不一致的問題,但還有另外一個問題:每次程式碼更改之後,都要重新獲取一遍 pom.xml 中描述的所有依賴項。下面我們來解決這個問題。
在單獨的步驟中獲取依賴項
結合前面提到的快取機制,我們可以讓獲取依賴項這一步變成可快取單元,只要 pom.xml 檔案的內容沒有變化,無論程式碼如何更改,都不會破壞這一層的快取。上圖中兩個 COPY 指令中間的 RUN 指令用來告訴 Maven 只獲取依賴項。
現在又遇到了一個新問題:跟之前直接拷貝 jar 包相比,映象體積變得更大了,因為它包含了很多執行應用時不需要的構建依賴項。
使用多階段構建來刪除構建時的依賴項
多階段構建可以由多個 FROM 指令識別,每一個 FROM 語句表示一個新的構建階段,階段名稱可以用 AS
引數指定。本例中指定第一階段的名稱為 builder
,它可以被第二階段直接引用。兩個階段環境一致,並且第一階段包含所有構建依賴項。
第二階段是構建最終映象的最後階段,它將包括應用執行時的所有必要條件,本例是基於 Alpine 的最小 JRE 映象。上一個構建階段雖然會有大量的快取,但不會出現在第二階段中。為了將構建好的 jar 包新增到最終的映象中,可以使用 COPY --from=STAGE_NAME
指令,其中 STAGE_NAME 是上一構建階段的名稱。
多階段構建是刪除構建依賴的首選方案。
本文從在非一致性環境中構建體積較大的映象開始優化,一直優化到在一致性環境中構建最小映象,同時充分利用了快取機制。下一篇文章將會介紹多階段構建的更多其他用途。