使用缓存管理来优化构建
您可能会发现自己一遍又一遍地重建相同的 Docker 镜像。 无论是用于软件的下一个版本,还是在本地开发过程中。 由于构建镜像是一项常见任务,因此 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 构建缓存。 一旦层发生变化,所有下游层也需要重建。 即使他们不会以不同的方式构建任何东西,他们仍然需要重新运行。
假设您的 Dockerfile 中有RUN apt-get update && apt-get upload -y
步骤,用于将基于 Debian 的镜像中的所有软件包升级到最新版本。
这并不意味着您构建的镜像始终是最新的。 一周后在同一主机上重建镜像仍将获得与以前相同的软件包。
强制重建的唯一方法是确保其之前的层已更改,或者使用 docker builder prune
清除构建缓存。
如何有效地使用缓存?
现在您已经了解了缓存的工作原理,您可以开始利用缓存来发挥自己的优势。 虽然缓存将自动在您运行的任何 docker 构建上工作,但您通常可以重构 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
等命令。 会将整个构建上下文COPY
到镜像中。
如果您的当前目录中有日志、包管理器 artifacts,甚至以前的构建结果,这些也将被复制。
这可能会使您的镜像比需要的更大,特别是因为这些文件通常没有用处。
通过明确说明要复制的文件或目录,避免将不必要的文件添加到构建中。
例如,您可能只想将Makefile
和src
目录添加到镜像文件系统。
在这种情况下,请考虑将其添加到您的 Dockerfile 中:
COPY ./src ./Makefile /src
与此相反:
COPY . /src
您还可以创建 .dockerignore
文件,并使用它来指定要从构建上下文中排除的文件和目录。
聪明的使用您的包管理器
大多数 Docker 镜像构建都涉及使用包管理器来帮助将软件安装到镜像中。
Debian 有apt
,Alpine 有apk
,Python 有pip
,NodeJS 有npm
,等等。
安装软件包时,要考虑全面。 确保只安装您需要的软件包。 如果您不打算使用它们,请不要安装它们。 请记住,对于您的本地开发环境和生产环境来说,这可能是不同的列表。 您可以使用多阶段构建来有效地拆分它们。
使用 RUN
专用缓存
RUN
命令支持专用缓存,当您在 runs 时需要更细粒度的缓存时,可以使用该缓存。
例如,在安装软件包时,您并不总是需要每次都从互联网上获取所有软件包。 您只需要已更改的内容。
要解决这个问题,可以使用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-temur。 即使没有您可能想要的官方镜像,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 功能是 heredoc。
它使您能够创建具有良好可读性的多行脚本:
RUN <<EOF
set -e
echo "the first command"
echo "the second command"
EOF
set -e
命令在任何命令失败后立即退出,而不是继续。