优化构建中的缓存使用
在使用 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 开头的文件和目录:
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 中的常规缓存层对应于指令及其依赖文件的精确匹配。如果指令及其依赖文件在构建该层后已更改,该层将失效,构建过程必须重新构建该层。
缓存挂载是一种指定在构建期间使用的持久化缓存位置的方法。缓存在多次构建之间是累积的,因此您可以多次读取和写入缓存。这种持久化缓存意味着即使您需要重新构建一个层,也只需下载新的或更改的包。任何未更改的包都会从缓存挂载中重用。
要在构建中使用缓存挂载,您可以在 Dockerfile 中的 --mount 指令使用 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/helloRUN --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 gccRUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtRUN --mount=type=cache,target=/root/.gem \
bundle installRUN --mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/usr/local/cargo/registry/ \
cargo buildRUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restoreRUN --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 注册表镜像:
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 .
摘要
在构建中优化缓存使用可以显著加快构建过程。 保持构建上下文较小,使用绑定挂载、缓存挂载和外部缓存都是您可以利用构建缓存并加快构建过程的技术。
有关本指南中讨论的概念的更多信息,请参阅: