优化构建中的缓存使用

使用 Docker 进行构建时,如果 指令,它所依赖的文件自以前以来没有更改 建立。重用缓存中的层可以加快构建过程,因为 Docker 不必再次重新构建图层。

以下是一些可用于优化构建缓存和加速的技术 构建过程:

  • 对层进行排序:将命令放入 Dockerfile 的逻辑顺序可以帮助你避免不必要的缓存 失效。
  • 保持上下文较小:上下文是 发送到生成器以处理构建的文件和目录 指令。保持上下文尽可能小可以减少 需要发送到构建器,并降低缓存的可能性 失效。
  • 使用绑定挂载:绑定挂载允许您挂载文件或 目录中的 build 容器。使用 bind 挂载 可以帮助您避免镜像中出现不必要的图层,这可能会减慢 build 过程。
  • 使用缓存挂载:缓存挂载允许您指定 在构建期间使用的持久包缓存。持久缓存有助于 加快构建步骤,尤其是涉及使用 一个 Package Manager 来执行。拥有包的持久缓存意味着即使 您重新构建一个层,您只下载新的或更改的包。
  • 使用外部缓存:外部缓存允许您 将 build cache 存储在远程位置。外部缓存镜像可以是 在多个 build 之间以及在不同的环境中共享。

对图层进行排序

将 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 效率相当低下。更新任何文件都会导致重新安装 所有依赖项,即使依赖项 自上次以来没有改变。

相反,该命令可以一分为二。首先,复制 package 管理文件(在本例中为 和 )。然后,安装 依赖关系。最后,复制项目源代码,即 subject 到频繁的变化。COPYpackage.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 的早期层中安装依赖项,可以 当项目文件发生更改时,无需重新构建这些图层。

保持上下文较小

确保上下文不包含不必要文件的最简单方法是 在构建上下文的根目录中创建文件。该文件的工作方式与文件类似,它允许您 从 build 上下文中排除 files 和 directories。.dockerignore.dockerignore.gitignore

下面是一个示例文件,其中排除了目录、所有文件和以 :.dockerignorenode_modulestmp

.dockerignore
node_modules
tmp*

文件中指定的 Ignore-rules 适用于整个构建 context,包括子目录。这意味着它是一个相当粗粒度的 机制,但这是排除您知道的文件和目录的好方法 在构建上下文中不需要临时文件、日志文件和 构建工件。.dockerignore

使用绑定挂载

您可能熟悉使用 Docker Compose 运行容器时的绑定挂载。绑定挂载允许您从 host machine 添加到容器中。docker run

# 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 中使用带有指令的标志:--mountRUN

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

在此示例中,当前目录已挂载到构建容器中 在执行命令之前。源代码位于 该指令持续时间的 build 容器。当 指令执行完,挂载的文件不会持久化在最终的 image 或 build 缓存中。仅命令的输出 仍然。go buildRUNgo build

Dockerfile 中的 and 说明允许您从 build context 添加到构建容器中。使用 bind 挂载有利于 构建缓存优化,因为您没有向 缓存。如果你的 build 上下文偏大,并且它只被使用 要生成工件,最好使用 Bind mounts 暂时 将生成构件所需的源代码挂载到构建中。如果你 用于将文件添加到构建容器中,BuildKit 将包含所有 ,即使这些文件未在最终镜像中使用。COPYADDCOPY

在 build 中使用 bind 挂载时,需要注意以下几点:

  • 默认情况下,绑定挂载是只读的。如果需要写入挂载的 目录中,您需要指定该选项。但是,即使使用该选项,更改也不会保留在最终镜像或构建缓存中。 文件写入在指令期间持续,并且 在指令完成后被丢弃。rwrwRUN

  • 挂载的文件不会保留在最终镜像中。只有指令的输出保留在最终镜像中。如果您需要包括 文件,您需要使用 OR 说明。RUNCOPYADD

  • 如果目标目录不为空,则目标目录的内容 被挂载的文件隐藏。指令完成后,将恢复原始内容。RUN

    例如,给定一个只有 a 的构建上下文:Dockerfile

    .
    └── Dockerfile

    以及将当前目录挂载到构建容器中的 Dockerfile:

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

    带有 bind 挂载的第一个命令显示挂载的 目录。第二个选项列出了原始生成上下文的内容。lsls

    构建日志
    #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 中的常规缓存层对应于指令的精确匹配 以及它所依赖的文件。如果 指令及其依赖的文件 自构建层以来已更改,则层无效,并且构建 进程必须重新构建层。

缓存挂载是指定要在 建立。缓存在构建之间是累积的,因此您可以读取和写入 cache 多次缓存。这种持久缓存意味着,即使您需要 rebuild a layer,则只需下载新的或更改的包。任何未更改 包将从缓存挂载中重用。

要在构建中使用缓存挂载,您可以在 Dockerfile 中使用带有指令的标志:--mountRUN

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

在此示例中,该命令使用目录的缓存挂载,即 npm 缓存的默认位置。缓存挂载 在构建中保留,因此即使您最终重新构建了层,您 仅下载新的或更改的软件包。对缓存的任何更改都会被持久化 跨 build,并且缓存在多个 build 之间共享。npm install/root/.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 需要 exclusive 访问其数据,因此缓存使用选项来确保 使用相同缓存挂载的并行构建会相互等待,而不是访问 相同的缓存文件。sharing=locked

使用外部缓存

构建的默认缓存存储是构建器内部的 (BuildKit instance)的每个构建器都使用自己的缓存存储。当您 在不同构建器之间切换,则缓存不会在它们之间共享。用 外部缓存允许您定义用于推送和拉取的远程位置 缓存数据。

外部缓存对于 CI/CD 管道特别有用,其中构建器 通常是短暂的,构建时间很宝贵。在 构建可以大大加快构建过程并降低成本。您甚至可以 在本地开发环境中使用相同的缓存。

要使用外部缓存,请在命令中指定 and 选项。--cache-to--cache-fromdocker buildx build

  • --cache-to将构建缓存导出到指定位置。
  • --cache-from指定 Build 要使用的远程缓存。

以下示例显示如何使用 设置 GitHub Actions 工作流,并将构建缓存层推送到 OCI 注册表 镜像:docker/build-push-action

.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-fromdocker buildx build

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

总结

优化构建中的缓存使用可以显著加快构建过程。 保持构建上下文较小,使用 bind mounts、cache 挂载和外部 缓存是您可以用来充分利用构建缓存和 加快构建过程。

有关本指南中讨论的概念的更多信息,请参阅: