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
文件,并使用它来指定要从构建上下文中排除哪些文件和目录。
明智地使用您的包管理器
大多数 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 个阶段: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
命令,它会在任何命令失败后立即退出,而不是继续运行。)
其他资源
有关使用缓存进行高效构建的更多信息,请参见: