原文連接:你肯定你會寫 Dockerfile 嗎?docker
現在 GitHub 倉庫中已經包含了成千上萬的 Dockerfile,但並非全部的 Dockerfile 都是高效的。本文將從五個方面來介紹 Dockerfile 的最佳實踐,以此來幫助你們編寫更優雅的 Dockerfile。若是你是 Docker 的初學者,恭喜你,這篇文章就是爲你準備的。後面的系列將會更加深刻,敬請期待!緩存
本文使用一個基於
Maven
的 Java 項目做爲示例,而後不斷改進 Dockerfile 的寫法,直到最後寫出一個最優雅的 Dockerfile。中間的全部步驟都是爲了說明某一方面的最佳實踐。安全
一個開發週期包括構建 Docker 鏡像,更改代碼,而後從新構建 Docker 鏡像。在構建鏡像的過程當中,若是可以利用緩存,能夠減小沒必要要的重複構建步驟。maven
鏡像的構建順序很重要,當你向 Dockerfile 中添加文件,或者修改其中的某一行時,那一部分的緩存就會失效,該緩存的後續步驟都會中斷,須要從新構建。因此優化緩存的最佳方法是把不須要常常更改的行放到最前面,更改最頻繁的行放到最後面。ide
當拷貝文件到鏡像中時,儘可能只拷貝須要的文件,切忌使用 COPY .
指令拷貝整個目錄。若是被拷貝的文件內容發生了更改,緩存就會被破壞。在上面的示例中,鏡像中只須要構建好的 jar 包,所以只須要拷貝這個文件就好了,這樣即便其餘不相關的文件發生了更改也不會影響緩存。工具
每個 RUN
指令都會被看做是可緩存的執行單元。太多的 RUN 指令會增長鏡像的層數,增大鏡像體積,而將全部的命令都放到同一個 RUN 指令中又會破壞緩存,從而延緩開發週期。當使用包管理器安裝軟件時,通常都會先更新軟件索引信息,而後再安裝軟件。推薦將更新索引和安裝軟件放在同一個 RUN 指令中,這樣能夠造成一個可緩存的執行單元,不然你可能會安裝舊的軟件包。post
鏡像的體積很重要,由於鏡像越小,部署的速度更快,攻擊範圍越小。優化
刪除沒必要要的依賴,不要安裝調試工具。若是實在須要調試工具,能夠在容器運行以後再安裝。某些包管理工具(如 apt
)除了安裝用戶指定的包以外,還會安裝推薦的包,這會平白無故增長鏡像的體積。apt 能夠經過添加參數 -–no-install-recommends
來確保不會安裝不須要的依賴項。若是確實須要某些依賴項,請在後面手動添加。ui
包管理工具會維護本身的緩存,這些緩存會保留在鏡像文件中,推薦的處理方法是在每個 RUN 指令的末尾刪除緩存。若是你在下一條指令中刪除緩存,不會減少鏡像的體積。操作系統
固然了,還有其餘更高級的方法能夠用來減少鏡像體積,以下文將會介紹的多階段構建。接下來咱們將探討如何優化 Dockerfile 的可維護性、安全性和可重複性。
使用官方鏡像能夠節省大量的維護時間,由於官方鏡像的全部安裝步驟都使用了最佳實踐。若是你有多個項目,能夠共享這些鏡像層,由於他們均可以使用相同的基礎鏡像。
基礎鏡像儘可能不要使用 latest
標籤。雖然這很方便,但隨着時間的推移,latest 鏡像可能會發生重大變化。所以在 Dockerfile 中最好指定基礎鏡像的具體標籤。咱們使用 openjdk
做爲示例,指定標籤爲 8。其餘更多標籤請查看官方倉庫。
基礎鏡像的標籤風格不一樣,鏡像體積就會不一樣。slim
風格的鏡像是基於 Debian 發行版製做的,而 alpine
風格的鏡像是基於體積更小的 Alpine Linux 發行版製做的。其中一個明顯的區別是:Debian 使用的是 GNU 項目所實現的 C 語言標準庫,而 Alpine 使用的是 Musl C 標準庫,它被設計用來替代 GNU C 標準庫(glibc)的替代品,用於嵌入式操做系統和移動設備。所以使用 Alpine 在某些狀況下會遇到兼容性問題。 以 openjdk 爲例,jre
風格的鏡像只包含 Java 運行時,不包含 SDK
,這麼作也能夠大大減小鏡像體積。
到目前爲止,咱們一直都在假設你的 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 是上一構建階段的名稱。
多階段構建是刪除構建依賴的首選方案。
本文從在非一致性環境中構建體積較大的鏡像開始優化,一直優化到在一致性環境中構建最小鏡像,同時充分利用了緩存機制。下一篇文章將會介紹多階段構建的更多其餘用途。