优化构建中的缓存使用
在使用Docker构建时,如果指令及其依赖的文件自上次构建以来没有更改,则会从构建缓存中重用该层。从缓存中重用层可以加快构建过程,因为Docker不需要再次重建该层。
以下是几种可以用来优化构建缓存并加快构建过程的技术:
- Order your layers: 将你的Dockerfile中的命令按逻辑顺序排列可以帮助你避免不必要的缓存失效。
- 保持上下文简洁: 上下文是发送给构建器以处理构建指令的一组文件和目录。尽可能保持上下文的小巧可以减少需要发送给构建器的数据量,并降低缓存失效的可能性。
- 使用绑定挂载: 绑定挂载允许你将主机上的文件或目录挂载到构建容器中。使用绑定挂载可以帮助你避免镜像中的不必要层,这可能会减慢构建过程。
- 使用缓存挂载: 缓存挂载允许您指定一个持久的包缓存,用于构建过程中。持久缓存有助于加快构建步骤,特别是涉及使用包管理器安装包的步骤。拥有持久的包缓存意味着即使您重新构建一个层,您只需下载新的或更改的包。
- 使用外部缓存: 外部缓存允许您将构建缓存存储在远程位置。外部缓存镜像可以在多个构建之间共享,并且跨不同的环境使用。
Order your layers
将命令按逻辑顺序放入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中使用RUN指令的--mount标志:
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列出了原始构建上下文的内容。Build log#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中使用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/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 .
摘要
优化构建中的缓存使用可以显著加快构建过程。保持构建上下文小,使用绑定挂载、缓存挂载和外部缓存都是您可以用来充分利用构建缓存并加快构建过程的技术。
有关本指南中讨论的概念的更多信息,请参阅: