本文使用一個基於 Maven 的 Java 項目作爲示例,然後不斷改進 Dockerfile 的寫法,直到最後寫出一個最優雅的 Dockerfile。中間的所有步驟都是爲了說明某一方面的最佳實踐。
一、減少構建時間
一個開發週期包括構建 Docker 鏡像,更改代碼,然後重新構建 Docker 鏡像。在構建鏡像的過程中,如果能夠利用緩存,可以減少不必要的重複構建步驟。
1、構建順序影響緩存的利用率
2、只拷貝需要的文件,防止緩存溢出
3、最小化可緩存的執行層
二、 減小鏡像體積
鏡像的體積很重要,因爲鏡像越小,部署的速度更快,***範圍越小。
1、刪除不必要依賴
2、刪除包管理工具的緩存
包管理工具會維護自己的緩存,這些緩存會保留在鏡像文件中,推薦的處理方法是在每一個 RUN 指令的末尾刪除緩存。如果你在下一條指令中刪除緩存,不會減小鏡像的體積。
當然了,還有其他更高級的方法可以用來減小鏡像體積,如下文將會介紹的多階段構建。接下來我們將探討如何優化 Dockerfile 的可維護性、安全性和可重複性。
三、可維護性
1、儘量使用官方鏡像
2、使用更具體的標籤
3、使用體積最小的基礎鏡像
基礎鏡像的標籤風格不同,鏡像體積就會不同。slim 風格的鏡像是基於 Debian 發行版製作的,而 alpine 風格的鏡像是基於體積更小的 Alpine Linux 發行版製作的。其中一個明顯的區別是:Debian 使用的是 GNU 項目所實現的 C 語言標準庫,而 Alpine 使用的是 Musl C 標準庫,它被設計用來替代 GNU C 標準庫(glibc)的替代品,用於嵌入式操作系統和移動設備。因此使用 Alpine 在某些情況下會遇到兼容性問題。 以 openjdk 爲例,jre 風格的鏡像只包含 Java 運行時,不包含 SDK,這麼做也可以大大減少鏡像體積。
四、重複利用
到目前爲止,我們一直都在假設你的 jar 包是在主機上構建的,這還不是理想方案,因爲沒有充分利用容器提供的一致性環境。例如,如果你的 Java 應用依賴於某一個特定的操作系統的庫,就可能會出現問題,因爲環境不一致(具體取決於構建 jar 包的機器)。
1、在一致的環境中從源代碼構建
源代碼是你構建 Docker 鏡像的最終來源,Dockerfile 裏面只提供了構建步驟。
首先應該確定構建應用所需的所有依賴,本文的示例 Java 應用很簡單,只需要 Maven 和 JDK,所以基礎鏡像應該選擇官方的體積最小的 maven 鏡像,該鏡像也包含了 JDK。如果你需要安裝更多依賴,可以在 RUN 指令中添加。pom.xml 文件和 src 文件夾需要被複制到鏡像中,因爲最後執行 mvn package 命令(-e 參數用來顯示錯誤,-B 參數表示以非交互式的“批處理”模式運行)打包的時候會用到這些依賴文件。
雖然現在我們解決了環境不一致的問題,但還有另外一個問題:**每次代碼更改之後,都要重新獲取一遍 pom.xml 中描述的所有依賴項。**下面我們來解決這個問題。
2、在單獨的步驟中獲取依賴項
現在又遇到了一個新問題:跟之前直接拷貝 jar 包相比,鏡像體積變得更大了,因爲它包含了很多運行應用時不需要的構建依賴項。
3、使用多階段構建來刪除構建時的依賴項
第二階段是構建最終鏡像的最後階段,它將包括應用運行時的所有必要條件,本例是基於 Alpine 的最小 JRE 鏡像。上一個構建階段雖然會有大量的緩存,但不會出現在第二階段中。爲了將構建好的 jar 包添加到最終的鏡像中,可以使用 COPY --from=STAGE_NAME 指令,其中 STAGE_NAME 是上一構建階段的名稱。