在建置過程中最佳化快取使用

使用 Docker 建置時,如果指令及其相依的檔案自上次建置以來沒有變更,則會從建置快取中重複使用層級。重複使用快取中的層級可以加快建置流程,因為 Docker 不需要再次重建該層級。

以下是一些您可以用來最佳化建置快取並加快建置流程的技巧

  • 排列您的層級:將 Dockerfile 中的指令按邏輯順序排列可以幫助您避免不必要的快取失效。
  • 保持上下文精簡:上下文是傳送給建置器以處理建置指令的檔案和目錄集。盡可能保持上下文精簡可以減少需要傳送給建置器的資料量,並降低快取失效的可能性。
  • 使用繫結掛載:繫結掛載可讓您將主機電腦上的檔案或目錄掛載到建置容器中。使用繫結掛載可以幫助您避免映像檔中不必要的層級,這可能會降低建置流程的速度。
  • 使用快取掛載:快取掛載可讓您指定要在建置期間使用的永久套件快取。永久快取有助於加快建置步驟,尤其是涉及使用套件管理器安裝套件的步驟。擁有套件的永久快取表示即使您重建層級,也只會下載新的或已變更的套件。
  • 使用外部快取:外部快取可讓您將建置快取儲存在遠端位置。外部快取映像檔可以在多個建置之間以及不同環境之間共用。

排列您的層級

將 Dockerfile 中的指令按邏輯順序排列是一個很好的起點。由於變更會導致後續步驟重建,因此請嘗試將耗時的步驟放在 Dockerfile 的開頭附近。經常變更的步驟應放在 Dockerfile 的結尾附近,以避免觸發未變更層級的重建。

請參考以下範例。一個 Dockerfile 程式碼片段,它從目前目錄中的原始碼檔案執行 JavaScript 建置

# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY . .          # Copy over all files in the current directory
RUN npm install   # Install dependencies
RUN npm build     # Run build

這個 Dockerfile 相當沒有效率。每次建置 Docker 映像檔時,更新任何檔案都會導致重新安裝所有相依性,即使相依性自上次以來沒有變更。

反之,可以將 `COPY` 指令分成兩個。首先,複製套件管理檔案(在本例中為 `package.json` 和 `yarn.lock`)。然後,安裝相依性。最後,複製專案原始碼,這部分經常變更。

# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY package.json yarn.lock .    # Copy package management files
RUN npm install                  # Install dependencies
COPY . .                         # Copy over project files
RUN npm build                    # Run build

藉由在 Dockerfile 的較早層級中安裝相依性,當專案檔案變更時,就不需要重建這些層級。

保持上下文精簡

確保您的上下文不包含不必要檔案的最簡單方法是在建置上下文的根目錄中建立一個 `.dockerignore` 檔案。`.dockerignore` 檔案的運作方式類似於 `.gitignore` 檔案,可讓您從建置上下文中排除檔案和目錄。

以下是一個範例 `.dockerignore` 檔案,它會排除 `node_modules` 目錄,以及所有以 `tmp` 開頭的檔案和目錄

.dockerignore
node_modules
tmp*

在 `.dockerignore` 檔案中指定的忽略規則適用於整個建置上下文,包括子目錄。這表示它是一個相當粗略的機制,但它是排除您知道在建置上下文中不需要的檔案和目錄的好方法,例如暫存檔、記錄檔和建置構件。

使用繫結掛載

您可能熟悉使用 `docker run` 或 Docker Compose 執行容器時的繫結掛載。繫結掛載可讓您將主機電腦上的檔案或目錄掛載到容器中。

# bind mount using the -v flag
docker run -v $(pwd):/path/in/container image-name
# bind mount using the --mount flag
docker run --mount=type=bind,src=.,dst=/path/in/container image-name

若要在建置中使用繫結掛載,您可以在 Dockerfile 中將 `--mount` 旗標與 `RUN` 指令一起使用

FROM golang:latest
WORKDIR /app
RUN --mount=type=bind,target=. go build -o /app/hello

在此範例中,目前目錄會在執行 `go build` 指令之前掛載到建置容器中。在該 `RUN` 指令執行期間,原始碼可在建置容器中使用。指令執行完成後,掛載的檔案不會保留在最終映像檔或建置快取中。只會保留 `go build` 指令的輸出。

在 Dockerfile 中,`COPY` 和 `ADD` 指令允許您將檔案從建置上下文複製到建置容器中。 使用繫結掛載有利於建置快取最佳化,因為您不會向快取中添加不必要的層。 如果您的建置上下文較大,並且僅用於產生成品,則最好使用繫結掛載將產生成品所需的原始程式碼暫時掛載到建置中。 如果您使用 `COPY` 將檔案添加到建置容器中,即使最終映像中未使用這些檔案,BuildKit 也會將所有這些檔案包含在快取中。

在建置中使用繫結掛載時,需要注意以下幾點

  • 繫結掛載預設為唯讀。 如果您需要寫入掛載的目錄,則需要指定 `rw` 選項。 但是,即使使用 `rw` 選項,變更也不會保留在最終映像或建置快取中。 檔案寫入會在 `RUN` 指令期間持續存在,並在指令完成後被捨棄。

  • 掛載的檔案不會保留在最終映像中。 只有 `RUN` 指令的輸出會保留在最終映像中。 如果您需要在最終映像中包含來自建置上下文的檔案,則需要使用 `COPY` 或 `ADD` 指令。

  • 如果目標目錄不為空,則目標目錄的內容會被掛載的檔案隱藏。 `RUN` 指令完成後,原始內容將會恢復。

    例如,假設一個建置上下文僅包含一個 `Dockerfile`

    .
    └── Dockerfile

    以及一個將目前目錄掛載到建置容器中的 Dockerfile

    FROM alpine:latest
    WORKDIR /work
    RUN touch foo.txt
    RUN --mount=type=bind,target=. ls
    RUN ls

    第一個帶有繫結掛載的 `ls` 指令顯示掛載目錄的內容。 第二個 `ls` 列出原始建置上下文的內容。

    建置日誌
    #8 [stage-0 3/5] RUN touch foo.txt
    #8 DONE 0.1s
    
    #9 [stage-0 4/5] RUN --mount=target=. ls -1
    #9 0.040 Dockerfile
    #9 DONE 0.0s
    
    #10 [stage-0 5/5] RUN ls -1
    #10 0.046 foo.txt
    #10 DONE 0.1s

使用快取掛載

Docker 中的常規快取層對應於指令及其所依賴檔案的完全匹配。 如果自建置該層以來,指令及其所依賴的檔案已更改,則該層將失效,並且建置過程必須重建該層。

快取掛載是一種指定在建置期間使用的永久快取位置的方法。 快取在各個建置之間是累積的,因此您可以多次讀寫快取。 這種永久快取意味著即使您需要重建一個層,您也只需下載新的或已更改的套件。 任何未更改的套件都將從快取掛載中重複使用。

要在建置中使用快取掛載,您可以將 `--mount` 旗標與 Dockerfile 中的 `RUN` 指令一起使用

FROM node:latest
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm npm install

在此範例中,`npm install` 指令對 `/root/.npm` 目錄(npm 快取的預設位置)使用快取掛載。 快取掛載會在各個建置之間持續存在,因此即使您最終重建了該層,也只需下載新的或已更改的套件。 對快取的任何更改都會在各個建置之間持續存在,並且快取會在多個建置之間共用。

您如何指定快取掛載取決於您使用的建置工具。 如果您不確定如何指定快取掛載,請參閱您正在使用的建置工具的文件。 以下是一些範例


RUN --mount=type=cache,target=/go/pkg/mod \
    go build -o /app/hello
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  apt update && apt-get --no-install-recommends install -y gcc
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
RUN --mount=type=cache,target=/root/.gem \
    bundle install
RUN --mount=type=cache,target=/app/target/ \
    --mount=type=cache,target=/usr/local/cargo/git/db \
    --mount=type=cache,target=/usr/local/cargo/registry/ \
    cargo build
RUN --mount=type=cache,target=/root/.nuget/packages \
    dotnet restore
RUN --mount=type=cache,target=/tmp/cache \
    composer install

請務必閱讀您正在使用的建置工具的文件,以確保您使用的是正確的快取掛載選項。 套件管理器對其使用快取的方式有不同的要求,使用錯誤的選項可能會導致意外的行為。 例如,Apt 需要獨佔訪問其數據,因此快取使用選項 `sharing=locked` 來確保使用相同快取掛載的並行建置會互相等待,而不是同時訪問相同的快取檔案。

使用外部快取

建置的預設快取儲存空間是您正在使用的建置器(BuildKit 執行個體)內部的。 每個建置器都使用自己的快取儲存空間。 當您在不同的建置器之間切換時,快取不會在它們之間共用。 使用外部快取可讓您定義用於推送和提取快取數據的遠端位置。

外部快取對於 CI/CD 管道特別有用,在這些管道中,建置器通常是臨時的,而且建置時間非常寶貴。 在建置之間重複使用快取可以大幅加快建置過程並降低成本。 您甚至可以在本機開發環境中使用相同的快取。

要使用外部快取,您可以使用 `--cache-to` 和 `--cache-from` 選項以及 `docker buildx build` 指令。

  • `--cache-to` 將建置快取匯出到指定位置。
  • `--cache-from` 指定建置要使用的遠端快取。

以下範例顯示如何使用 `docker/build-push-action` 設定 GitHub Actions 工作流程,並將建置快取層推送至 OCI 註冊表映像

.github/workflows/ci.yml
name: ci

on:
  push:

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: user/app:latest
          cache-from: type=registry,ref=user/app:buildcache
          cache-to: type=registry,ref=user/app:buildcache,mode=max

此設定指示 BuildKit 在 `user/app:buildcache` 映像中尋找快取。 建置完成後,新的建置快取會被推送至相同的映像,覆蓋舊的快取。

此快取也可以在本機使用。 若要在本機建置中提取快取,您可以將 `--cache-from` 選項與 `docker buildx build` 指令一起使用

$ docker buildx build --cache-from type=registry,ref=user/app:buildcache .

摘要

最佳化建置中的快取使用可以顯著加快建置過程。 保持建置上下文較小、使用繫結掛載、快取掛載和外部快取都是您可以用來充分利用建置快取並加快建置過程的技巧。

有關本指南中討論的概念的更多資訊,請參閱