構建Docker幾個小技巧

banq發表於2024-04-23

每當您構建 Docker 映像時,例如,您想要將 Java/Node/Python 應用程式整合為一個,您都會遇到以下兩個問題:

  • 如何使docker build命令執行得儘可能快?
  • 如何確保生成的 Docker 映象儘可能小?

Docker 映象層 101
看看下面的內容Dockerfile。

FROM eclipse-temurin:17-jdk
ARG JAR_FILE=build/libs<font>/*.jar
COPY  ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

透過執行docker build -t myapp .此 Dockerfile,您將獲得(一個)Docker映象,該映象將基於 Java 17 (Eclipse-Temurin) 映象,幷包含並執行我們的 Java 應用程式(app.jar 檔案)。

  • Docker 行中的每一行都將建立一個Docker 層
  • 每個映象都由幾個這樣的層組成。

您可以透過執行來確認這一點,例如:

 docker image history myapp

這將在新行上返回層:

IMAGE          CREATED              CREATED BY                                      SIZE      COMMENT
3ca5a60826f0   8 minutes ago   ENTRYPOINT ["java" "-jar" "/app.jar"]           0B        buildkit.dockerfile.v0
<missing>      8 minutes ago   COPY build/libs/*.jar app.jar # buildkit        19.7MB    buildkit.dockerfile.v0
<missing>      8 minutes ago   ARG JAR_FILE=build/libs/*.jar                   0B        buildkit.dockerfile.v0
... (other layers from the base image left out)

我們的映象中

  • 有一層ENTRYPOINT、一層用於COPY、一層用於ARG。
  • 包含 app.jar 檔案(COPY)的圖層大約有 20MB,ENTRYPOINT 和 ARG 行的後設資料圖層為 0B。

現在,我們如何處理這些資訊?

你的層很容易膨脹
想象一下,您想要透過包管理器安裝一個包,為此,您想要執行apt update,它會更新包管理器的索引。

FROM eclipse-temurin:17-jdk
RUN apt update -y
ARG JAR_FILE=build/libs/*.jar
COPY  ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

讓我們看一下生成的圖層 ( docker image history myapp),並重點關注最後一行 ( RUN /bin/sh -c…​):

IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
c14a18a04751   8 seconds ago   ENTRYPOINT ["java" "-jar" "/app.jar"]           0B        buildkit.dockerfile.v0
<missing>      8 seconds ago   COPY build/libs/*.jar app.jar # buildkit        19.7MB    buildkit.dockerfile.v0
<missing>      8 seconds ago   ARG JAR_FILE=build/libs/*.jar                   0B        buildkit.dockerfile.v0
<missing>      8 seconds ago   RUN /bin/sh -c apt update -y # buildkit         45.7MB    buildkit.dockerfile.v0

哇哈! Runningapt-update為我們生成的 Docker 映象新增了一個大小高達 45.7MB 的新層。

現在,每次推送或拉取映象時,您都需要傳輸這些額外的兆​​位元組。

圖層是相加的
讓我們繼續上面的示例並新增更多執行命令,以安裝最新的 mysql 軟體包。

FROM eclipse-temurin:17-jdk
RUN apt update -y
RUN apt install mysql -y
RUN rm -rf /var/lib/apt/lists/*
ARG JAR_FILE=build/libs/*.jar
COPY  ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

此外,我們還使用該rm -rf /var/lib/apt/lists/*命令刪除 apt 索引快取(上面的 45.7MB)。讓我們看看我們的映象歷史現在是什麼樣子:

59f82a5b4c5a   6 seconds ago   ENTRYPOINT ["java" "-jar" "/app.jar"]           0B        buildkit.dockerfile.v0
<missing>      6 seconds ago   COPY build/libs/*.jar app.jar # buildkit        19.7MB    buildkit.dockerfile.v0
<missing>      6 seconds ago   ARG JAR_FILE=build/libs/*.jar                   0B        buildkit.dockerfile.v0
<missing>      6 seconds ago   RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #…   0B        buildkit.dockerfile.v0
<missing>      7 seconds ago   RUN /bin/sh -c apt install -y mysql-server #…   605MB     buildkit.dockerfile.v0
<missing>      8 minutes ago   RUN /bin/sh -c apt update -y # buildkit         45.7MB    buildkit.dockerfile.v0

哇啊,那是什麼?即使我們刪除了 apt 快取檔案,45.7MB 層仍然存在(除了 605MB MySQL 層,順便說一句)。

這是因為層是嚴格可加的/不可變的。您當然可以從當前圖層中刪除這些檔案,但較舊/之前的圖層仍將包含它們。

如何解決這個問題?
一個簡單的解決方法是RUN在一行上執行所有三個命令(==單個結果層)

FROM eclipse-temurin:17-jdk
RUN apt update -y &&  \
    apt install -y mysql-server &&  \
    rm -rf /var/lib/apt/lists/*
ARG JAR_FILE=build/libs/*.jar
COPY  ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

現在讓我們看看該影像的歷史:

IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
4b8c0f7f895a   14 seconds ago   ENTRYPOINT ["java" "-jar" "/app.jar"]           0B        buildkit.dockerfile.v0
<missing>      14 seconds ago   COPY build/libs/*.jar app.jar # buildkit        19.7MB    buildkit.dockerfile.v0
<missing>      14 seconds ago   ARG JAR_FILE=build/libs/*.jar                   0B        buildkit.dockerfile.v0
<missing>      14 seconds ago   RUN /bin/sh -c apt update -y &&      apt ins…   605MB     buildkit.dockerfile.v0

哈!我們現在至少節省了 45.7MB。但這還有什麼問題呢?

使其可重複
理想情況下,您希望您的構建是可重現的(誰會想到)。透過執行apt update然後安裝儲存庫中的任何最新軟體包,您實際上會破壞這種可重複性,因為軟體包版本可能會在構建之間發生變化。

要旨:

  1. 僅安裝您要安裝的特定版本
  2. 首先避免在 Dockerfile 中為你的應用程式新增(你所選擇的軟體包管理器)相反,構建一個新的基礎映象,並在你的 Dockerfile 的 FROM 中使用它。這樣速度也會快很多!

圖層順序很重要
您需要確保將變化較大的圖層放置在底部Dockerfile,而更穩定的圖層應放置在頂部。

為什麼?因為在構建映象時,您需要從構建之間更改的層開始重建每個層。

一個實際的例子:想象一下,您想要將一個index.html檔案打包到您的映象中,該檔案變化很大,即比其他任何內容都更頻繁。

FROM eclipse-temurin:17-jdk
COPY index.html index.html
RUN apt update -y &&  \
    apt install -y mysql-server &&  \
    rm -rf /var/lib/apt/lists/*
ARG JAR_FILE=build/libs/*.jar
COPY  ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

您可以看到該COPY index.html index.html行幾乎新增到了 的頂部Dockerfile。
現在,每次index.html 檔案發生更改時,您都需要重建所有後續層,即圖層_RUN apt-update, ARG & COPY app.jar
 這是一個巨大的時間消耗:在我的機器上,以上所有操作大約需要 17 秒才能完成。

但是,如果您將語句重新排序到底部,Docker 可以重新使用所有先前的層,因為它們沒有改變。

FROM eclipse-temurin:17-jdk
RUN apt update -y &&  \
    apt install -y mysql-server &&  \
    rm -rf /var/lib/apt/lists/*
ARG JAR_FILE=build/libs/*.jar
COPY  ${JAR_FILE} app.jar
COPY index.html index.html
ENTRYPOINT ["java","-jar","/app.jar"]

現在一個新的docker build只需要 0.5 秒(在我的機器上),好多了!

以下是黃金分層規則:

  • 很少更改或時間/網路密集型的檔案(例如安裝新軟體) → 頂部
  • 經常更改的檔案(例如原始碼)→ 非常低
  • ENV、CMD 等 → 底部

Docker什麼時候重新構建層?
每當您執行docker build.關於 Docker 何時以及如何快取層有一組特定的規則,您可以在官方文件中閱讀它們。

要點是,每當您執行 Docker 構建時,Docker 都會:

  • 檢查 Dockerfile 中的命令是否有更改(例如,你是否將 RUN blah 改為 RUN doh)。
  • 在 ADD 或 COPY 的情況下,是否有任何涉及的檔案(或者說它們的校驗和)發生了變化?

.dockerignore
當你執行 docker build -t <tag> .時,你的當前目錄 .實際上就是所謂的構建上下文。這意味著當前目錄下的所有檔案都將被壓縮併傳送給本地或遠端的 Docker 守護程序來執行構建。

如果你想確保某些目錄永遠不會被髮送到你的構建守護程序,從而保持快速和小巧,你可以建立一個 .dockerignore 檔案,其語法與 .gitignore 類似。

一般來說,你應該把與編譯無關的檔案/目錄放在這裡(比如你的 .git 資料夾),這在使用 COPY ./somewhere 等命令時尤為重要,因為這樣一來,整個專案都會出現在生成的映像中。

以 npm 為例:例如,你可能想在構建時執行 npm install,讓它下載其依賴項,而不是(慢慢地)將你的 node_modules 資料夾複製進去,因此這也是 dockerignore 檔案的一個很好的候選項。不過,如果你這麼做了,還有一個技巧你一定要知道:目錄快取。

目錄快取
例如,您執行 npm install、pip install gradlew build 等來構建映象。這將導致下載依賴項並建立新的映像層。現在,如果要重建該映像層,所有依賴項都將在下一次構建時重新下載,因為已經下載的依賴項中沒有 .npm、.cache 或 .gradle 資料夾可用。

但你可以改變這種情況!讓我們以 pip 為例,修改以下一行:

FROM ...
RUN pip install -r requirements.txt
CMD ...

到:

RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt

這將告訴 Docker 在構建過程中將快取層/資料夾(/root/.cache)掛載到容器中,在本例中,就是 pip 為根使用者快取其依賴項的資料夾。訣竅在於:這個資料夾最終不會出現在生成的映像中,但/root/.cache 會在所有後續構建中提供給 pip,這樣你就能獲得不錯的速度!

NPM、Gradle 或其他軟體包管理器也是如此。只需確保指定正確的目標資料夾即可。

相關文章