Docker 构建缓存
当您多次构建相同的 Docker 映像时,了解如何优化构建缓存是确保构建快速运行的好工具。
构建缓存如何工作
了解 Docker 的构建缓存有助于您编写更好的 Dockerfile,从而加快构建速度。
以下示例显示了用 C 编写的程序的一个小型 Dockerfile。
# syntax=docker/dockerfile:1
FROM ubuntu:latest
RUN apt-get update && apt-get install -y build-essentials
COPY main.c Makefile /src/
WORKDIR /src/
RUN make build
此 Dockerfile 中的每条指令都会转换为最终映像中的一个层。您可以将图像层视为一个堆栈,每个层在其之前的层之上添加更多内容:


每当层发生变化时,就需要重新构建该层。例如,假设您对main.c
文件中的程序进行了更改。进行此更改后,COPY
必须再次运行该命令才能使这些更改出现在图像中。换句话说,Docker 将使该层的缓存失效。
如果某个图层发生更改,则该图层之后的所有其他图层也会受到影响。当使用该命令的层COPY
失效时,后面的所有层也需要重新运行:


简而言之,这就是 Docker 构建缓存。一旦层发生变化,所有下游层也需要重建。即使他们不会以不同的方式构建任何东西,他们仍然需要重新运行。
有关缓存失效如何工作的更多详细信息,请参阅 缓存失效。
优化构建缓存的使用方式
现在您已经了解了缓存的工作原理,您可以开始利用缓存来发挥自己的优势。虽然缓存会自动作用于docker build
您运行的任何文件,但您通常可以重构 Dockerfile 以获得更好的性能。这些优化可以为您的构建节省宝贵的几秒钟(甚至几分钟)。
订购图层
将 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.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 的早期层中安装依赖项,当项目文件发生更改时,无需重建这些层。
保持层数较小
加快映像构建速度的最佳方法之一就是在构建中放入更少的内容。更少的部件意味着缓存保持更小,但也意味着可能过时和需要重建的东西应该更少。
首先,这里有一些提示和技巧:
不要包含不必要的文件
请考虑添加到图像中的文件。
运行类似的命令COPY . /src
会将整个
构建上下文复制
到映像中。如果您的当前目录中有日志、包管理器工件,甚至以前的构建结果,这些也将被复制。这可能会使您的图像比需要的更大,特别是因为这些文件通常没有用。
通过明确说明要复制的文件或目录,避免将不必要的文件添加到构建中。例如,您可能只想将目录添加
Makefile
到src
图像文件系统。在这种情况下,请考虑将其添加到您的 Dockerfile 中:
COPY ./src ./Makefile /src
与此相反:
COPY . /src
您还可以创建一个
.dockerignore
file,并使用它来指定要从构建上下文中排除的文件和目录。
明智地使用你的包管理器
大多数 Docker 映像构建都涉及使用包管理器来帮助将软件安装到映像中。 Debian 有apt
,Alpine 有apk
,Python 有pip
,NodeJS 有npm
,等等。
安装软件包时,要考虑周全。确保只安装您需要的软件包。如果您不打算使用它们,请不要安装它们。请记住,对于您的本地开发环境和生产环境来说,这可能是不同的列表。您可以使用多阶段构建来有效地拆分它们。
使用专用的RUN缓存
该RUN
命令支持专门的缓存,当您在运行之间需要更细粒度的缓存时,可以使用它。例如,在安装软件包时,您并不总是需要每次都从互联网上获取所有软件包。您只需要已更改的内容。
为了解决这个问题,您可以使用RUN --mount type=cache
.例如,对于基于 Debian 的映像,您可以使用以下内容:
RUN \
--mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y git
使用带有标志的显式缓存可以在构建之间保留目录--mount
的内容
。target
当该层需要重建时,它将apt
使用/var/cache/apt
.
尽量减少层数
保持较小的层数是一个很好的第一步,合乎逻辑的下一步是减少所拥有的层数。更少的层意味着当 Dockerfile 中的某些内容发生更改时,您需要重建的内容更少,因此您的构建将更快完成。
以下部分概述了一些可用于将层数保持在最低限度的技巧。
使用合适的基础镜像
Docker为几乎所有常见的开发场景提供了超过 170 个预构建的
官方镜像。例如,如果您正在构建 Java Web 服务器,请使用专用映像,例如
eclipse-temurin
.即使没有您可能想要的官方镜像,Docker 也会提供来自经过
验证的发布商
和
开源合作伙伴的镜像
,为您提供帮助。 Docker 社区也经常生成第三方镜像来使用。
使用官方图像可以节省您的时间,并确保您在默认情况下保持最新状态和安全。
使用多阶段构建
多阶段构建可让您将 Dockerfile 分成多个不同的阶段。每个阶段都会完成构建过程中的一个步骤,您可以桥接不同的阶段以在最后创建最终图像。 Docker 构建器将计算出各个阶段之间的依赖关系,并使用最有效的策略来运行它们。这甚至允许您同时运行多个构建。
多阶段构建使用两个或多个FROM
命令。以下示例说明了构建一个简单的 Web 服务器,该服务器从docs
Git 中的目录提供 HTML:
# syntax=docker/dockerfile:1
# stage 1
FROM alpine as git
RUN apk add git
# stage 2
FROM git as fetch
WORKDIR /repo
RUN git clone https://github.com/your/repository.git .
# stage 3
FROM nginx as site
COPY --from=fetch /repo/docs/ /usr/share/nginx/html
此构建有 3 个阶段:git
、fetch
和site
。在此示例中,git
是舞台的基础fetch
。它使用该COPY --from
标志将数据从docs/
目录复制到 Nginx 服务器目录中。
每个阶段只有几条指令,并且在可能的情况下,Docker 将并行运行这些阶段。只有阶段中的指令site
才会最终作为最终图像中的图层。整个git
历史记录不会嵌入到最终结果中,这有助于保持图像小且安全。
尽可能将命令组合在一起
大多数 Dockerfile 命令,尤其RUN
是命令,通常可以组合在一起。例如,不要像RUN
这样使用:
RUN echo "the first command"
RUN echo "the second command"
可以在单个命令中运行这两个命令RUN
,这意味着它们将共享相同的缓存!这可以通过使用&&
shell 操作符运行一个又一个命令来实现:
RUN echo "the first command" && echo "the second command"
# or to split to multiple lines
RUN echo "the first command" && \
echo "the second command"
另一个允许您以简洁的方式简化和连接命令的 shell 功能是
heredocs
。它使您能够创建具有良好可读性的多行脚本:
RUN <<EOF
set -e
echo "the first command"
echo "the second command"
EOF
(请注意该set -e
命令在任何命令失败后立即退出,而不是继续。)
其他资源
有关使用缓存进行高效构建的更多信息,请参阅: