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 中的每个指令都会转换为最终映像中的一个层。您可以将映像层视为一个堆栈,每个层都在之前层的顶部添加更多内容

Image layer diagram

只要层发生更改,就需要重新构建该层。例如,假设您对 main.c 文件中的程序进行了更改。在此更改之后,COPY 命令将需要再次运行,以便这些更改出现在映像中。换句话说,Docker 将使此层的缓存失效。

如果一个层发生更改,则所有在其后的层也会受到影响。当包含 COPY 命令的层失效时,所有后续层也需要再次运行

Image layer diagram, showing cache invalidation

这就是 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.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 的早期层中安装依赖项,当项目文件发生更改时,无需重建这些层。

保持层的大小

加速映像构建的最佳方法之一就是将更少的内容放入构建中。更少的部件意味着缓存会更小,而且应该会有更少的内容可能过时且需要重建。

要开始使用,以下是一些提示和技巧

不要包含不必要的文件

请注意您添加到映像中的文件。

运行类似 COPY . /src 的命令会将您的整个 构建上下文 复制到映像中。如果您在当前目录中包含日志、包管理器工件,甚至之前的构建结果,这些内容也会被复制。这可能会使您的映像比实际需要的大,尤其是在这些文件通常没有用时。

通过明确说明要复制的文件或目录,避免向构建添加不必要的文件。例如,您可能只想将 Makefilesrc 目录添加到映像文件系统中。在这种情况下,请考虑将以下内容添加到您的 Dockerfile 中

COPY ./src ./Makefile /src

而不是这个

COPY . /src

您还可以创建一个 .dockerignore 文件,并使用它来指定要从构建上下文中排除哪些文件和目录。

明智地使用您的包管理器

大多数 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 目录的内容。当此层需要重建时,它将使用 /var/cache/apt 中的 apt 缓存。

最小化层数

保持层的大小是第一步,下一步是减少层的数量。更少的层意味着当 Dockerfile 中的内容发生更改时,您需要重建的内容更少,因此您的构建将更快完成。

以下部分概述了一些可用于将层数降至最低的提示。

使用合适的基映像

Docker 提供了超过 170 个预构建的 官方映像,几乎涵盖所有常见的开发场景。例如,如果您要构建一个 Java Web 服务器,请使用专门的映像,例如 eclipse-temurin。即使没有您想要的官方映像,Docker 也提供来自 认证发布者开源合作伙伴,可以帮助您入门。Docker 社区通常也会生成第三方映像以供使用。

使用官方映像可以节省您的时间,并确保您默认情况下保持最新和安全。

使用多阶段构建

多阶段构建 使您能够将 Dockerfile 拆分为多个不同的阶段。每个阶段完成构建过程中的一个步骤,您可以桥接不同的阶段以在最后创建您的最终映像。Docker 构建器将找出阶段之间的依赖关系,并使用最有效的策略运行它们。这甚至允许您同时运行多个构建。

多阶段构建使用两个或多个 FROM 命令。以下示例说明了构建一个简单的 Web 服务器,该服务器从 Git 中的 docs 目录提供 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 个阶段:gitfetchsite。在此示例中,gitfetch 阶段的基础。它使用 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 命令,它会在任何命令失败后立即退出,而不是继续运行。)

其他资源

有关使用缓存进行高效构建的更多信息,请参见: