在建置中最佳化快取使用

使用 Docker 進行建構時,如果指令及其依賴的檔案自上次建構後沒有變更,則會從建構快取中重複使用該層。重複使用快取中的層可以加快建構過程,因為 Docker 不需要重新建構該層。

以下是一些可用於優化建構快取並加速建構過程的技巧:

  • 安排層的順序:將 Dockerfile 中的指令按邏輯順序排列,有助於避免不必要的快取失效。
  • 保持建構上下文簡潔:上下文是發送到建構器以處理建構指令的檔案和目錄集合。盡量保持上下文精簡,可減少發送到建構器的資料量,並降低快取失效的可能性。
  • 使用綁定掛載 (Bind mounts):綁定掛載允許您將主機機器上的檔案或目錄掛載到建構容器中。使用綁定掛載有助於避免映像中出現不必要的層,這些層可能會拖慢建構過程。
  • 使用快取掛載 (Cache mounts):快取掛載允許您指定在建構期間使用的持久性套件快取。這種持久性快取有助於加速建構步驟,特別是涉及使用套件管理器安裝套件的步驟。擁有持久性的套件快取意味著即使您重新建構某一層,也只需下載新增或變更過的套件。
  • 使用外部快取:外部快取允許您將建構快取儲存在遠端位置。外部快取映像可以在多個建構之間以及不同的環境中共享。

安排層的順序

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

考慮以下範例。這是一個從目前目錄中的原始檔執行 JavaScript 建構的 Dockerfile 片段:

# 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.jsonyarn.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 檔案中指定的忽略規則適用於整個建構上下文,包括子目錄。這是一個較為粗略的機制,但卻是排除不需要出現在建構上下文中的檔案(如暫存檔、紀錄檔和建構產物)的好方法。

使用綁定掛載 (Bind mounts)

如果您使用 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 的 RUN 指令中使用 --mount 旗標:

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

在這個範例中,在執行 go build 指令之前,目前的目錄會被掛載到建構容器中。原始程式碼在該 RUN 指令期間可在建構容器中使用。當指令執行完畢後,掛載的檔案不會保留在最終映像或建構快取中,僅保留 go build 指令的輸出結果。

Dockerfile 中的 COPYADD 指令可讓您將檔案從建構上下文複製到建構容器中。使用綁定掛載有利於優化建構快取,因為您沒有向快取添加不必要的層。如果您的建構上下文較大,且僅用於產生構件,最好使用綁定掛載將產生構件所需的原始程式碼暫時掛載到建構過程中。如果您使用 COPY 將這些檔案加入建構容器,BuildKit 會將所有這些檔案包含在快取中,即使這些檔案並未用於最終映像。

使用建構中的綁定掛載時,有幾點需要注意:

  • 預設情況下,綁定掛載是唯讀的。如果您需要寫入掛載的目錄,則需要指定 rw 選項。然而,即使使用了 rw 選項,變更也不會保留在最終映像或建構快取中。檔案寫入僅在 RUN 指令期間有效,指令執行結束後即被捨棄。

  • 掛載的檔案不會保留在最終映像中。只有 RUN 指令的輸出結果會被保留。如果您需要將建構上下文中的檔案包含在最終映像中,則必須使用 COPYADD 指令。

  • 如果目標目錄不為空,則目標目錄的內容會被掛載的檔案隱藏。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

使用快取掛載 (Cache mounts)

Docker 中的一般快取層對應於指令及其所依賴檔案的精確匹配。如果指令及其依賴的檔案自該層建構後發生了變化,該層即失效,建構過程必須重新建構該層。

快取掛載 (Cache mounts) 是一種指定在建構期間使用的持久性快取位置的方法。該快取在多次建構之間是累積的,因此您可以多次讀取和寫入快取。這種持久性快取意味著即使您需要重新建構某一層,也只需下載新增或變更過的套件。任何未變更的套件都會直接從快取掛載中重複使用。

要在建構中使用快取掛載,可以在 Dockerfile 的 RUN 指令中使用 --mount 旗標:

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 管道特別有用,因為其中的建構器通常是短暫存在的,且建構時間極為寶貴。在建構之間重複使用快取可以大幅加快建構過程並降低成本。您甚至可以在本地開發環境中使用相同的快取。

要使用外部快取,請在使用 docker buildx build 指令時指定 --cache-to--cache-from 選項。

  • --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 映像中尋找快取。當建構完成後,新的建構快取會被推送到同一個映像,覆蓋舊的快取。

此快取也可以在本地使用。若要在本地建構中拉取快取,您可以在 docker buildx build 指令中使用 --cache-from 選項:

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

總結

優化建構時的快取使用可以顯著加快建構過程。保持建構上下文簡潔、使用綁定掛載、快取掛載和外部快取,都是您可以善用建構快取並加速建構過程的技巧。

如需了解本指南中討論的概念的更多資訊,請參閱:

© . This site is unofficial and not affiliated with Kubernetes or Docker Inc.