构建Docker几个小技巧
每当您构建 Docker 映像时,例如,您想要将 Java/Node/Python 应用程序整合为一个,您都会遇到以下两个问题:
- 如何使docker build命令运行得尽可能快?
- 如何确保生成的 Docker 镜像尽可能小?
Docker 镜像层 101
看看下面的内容Dockerfile。
FROM eclipse-temurin:17-jdk |
通过运行docker build -t myapp .此 Dockerfile,您将获得(一个)Docker镜像,该镜像将基于 Java 17 (Eclipse-Temurin) 镜像,并包含并运行我们的 Java 应用程序(app.jar 文件)。
- Docker 行中的每一行都将创建一个Docker 层
- 每个镜像都由几个这样的层组成。
您可以通过运行来确认这一点,例如:
docker image history myapp
这将在新行上返回层:
IMAGE CREATED CREATED BY SIZE COMMENT |
我们的镜像中
- 有一层ENTRYPOINT、一层用于COPY、一层用于ARG。
- 包含 app.jar 文件(COPY)的图层大约有 20MB,ENTRYPOINT 和 ARG 行的元数据图层为 0B。
现在,我们如何处理这些信息?
你的层很容易膨胀
想象一下,您想要通过包管理器安装一个包,为此,您想要运行apt update,它会更新包管理器的索引。
FROM eclipse-temurin:17-jdk |
让我们看一下生成的图层 ( docker image history myapp),并重点关注最后一行 ( RUN /bin/sh -c…):
IMAGE CREATED CREATED BY SIZE COMMENT |
哇哈! Runningapt-update为我们生成的 Docker 镜像添加了一个大小高达 45.7MB 的新层。
现在,每次推送或拉取镜像时,您都需要传输这些额外的兆字节。
图层是相加的
让我们继续上面的示例并添加更多运行命令,以安装最新的 mysql 软件包。
FROM eclipse-temurin:17-jdk |
此外,我们还使用该rm -rf /var/lib/apt/lists/*命令删除 apt 索引缓存(上面的 45.7MB)。让我们看看我们的镜像历史现在是什么样子:
59f82a5b4c5a 6 seconds ago ENTRYPOINT ["java" "-jar" "/app.jar"] 0B buildkit.dockerfile.v0 |
哇啊,那是什么?即使我们删除了 apt 缓存文件,45.7MB 层仍然存在(除了 605MB MySQL 层,顺便说一句)。
这是因为层是严格可加的/不可变的。您当然可以从当前图层中删除这些文件,但较旧/之前的图层仍将包含它们。
如何解决这个问题?
一个简单的解决方法是RUN在一行上运行所有三个命令(==单个结果层)
FROM eclipse-temurin:17-jdk |
现在让我们看看该图像的历史:
IMAGE CREATED CREATED BY SIZE COMMENT |
哈!我们现在至少节省了 45.7MB。但这还有什么问题呢?
使其可重复
理想情况下,您希望您的构建是可重现的(谁会想到)。通过运行apt update然后安装存储库中的任何最新软件包,您实际上会破坏这种可重复性,因为软件包版本可能会在构建之间发生变化。
要旨:
- 仅安装您要安装的特定版本
- 首先避免在 Dockerfile 中为你的应用程序添加(你所选择的软件包管理器)相反,构建一个新的基础镜像,并在你的 Dockerfile 的 FROM 中使用它。这样速度也会快很多!
图层顺序很重要
您需要确保将变化较大的图层放置在底部Dockerfile,而更稳定的图层应放置在顶部。
为什么?因为在构建镜像时,您需要从构建之间更改的层开始重建每个层。
一个实际的例子:想象一下,您想要将一个index.html文件打包到您的镜像中,该文件变化很大,即比其他任何内容都更频繁。
FROM eclipse-temurin:17-jdk |
您可以看到该COPY index.html index.html行几乎添加到了 的顶部Dockerfile。
现在,每次index.html 文件发生更改时,您都需要重建所有后续层,即图层_RUN apt-update, ARG & COPY app.jar
这是一个巨大的时间消耗:在我的机器上,以上所有操作大约需要 17 秒才能完成。
但是,如果您将语句重新排序到底部,Docker 可以重新使用所有先前的层,因为它们没有改变。
FROM eclipse-temurin:17-jdk |
现在一个新的docker build只需要 0.5 秒(在我的机器上),好多了!
以下是黄金分层规则:
- 很少更改或时间/网络密集型的文件(例如安装新软件) → 顶部
- 经常更改的文件(例如源代码)→ 非常低
- ENV、CMD 等 → 底部
Docker什么时候重新构建层?
每当您运行docker build.关于 Docker 何时以及如何缓存层有一组特定的规则,您可以在官方文档中阅读它们。
要点是,每当您运行 Docker 构建时,Docker 都会:
- 检查 Dockerfile 中的命令是否有更改(例如,你是否将 RUN blah 改为 RUN doh)。
- 在 ADD 或 COPY 的情况下,是否有任何涉及的文件(或者说它们的校验和)发生了变化?
.dockerignore
当你运行 docker build -t <tag> .时,你的当前目录 .实际上就是所谓的构建上下文。这意味着当前目录下的所有文件都将被压缩并发送给本地或远程的 Docker 守护进程来执行构建。
如果你想确保某些目录永远不会被发送到你的构建守护进程,从而保持快速和小巧,你可以创建一个 .dockerignore 文件,其语法与 .gitignore 类似。
一般来说,你应该把与编译无关的文件/目录放在这里(比如你的 .git 文件夹),这在使用 COPY ./somewhere 等命令时尤为重要,因为这样一来,整个项目都会出现在生成的映像中。
以 npm 为例:例如,你可能想在构建时运行 npm install,让它下载其依赖项,而不是(慢慢地)将你的 node_modules 文件夹复制进去,因此这也是 dockerignore 文件的一个很好的候选项。不过,如果你这么做了,还有一个技巧你一定要知道:目录缓存。
目录缓存
例如,您运行 npm install、pip install gradlew build 等来构建镜像。这将导致下载依赖项并创建新的映像层。现在,如果要重建该映像层,所有依赖项都将在下一次构建时重新下载,因为已经下载的依赖项中没有 .npm、.cache 或 .gradle 文件夹可用。
但你可以改变这种情况!让我们以 pip 为例,修改以下一行:
FROM ... |
到:
RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt |
这将告诉 Docker 在构建过程中将缓存层/文件夹(/root/.cache)挂载到容器中,在本例中,就是 pip 为根用户缓存其依赖项的文件夹。诀窍在于:这个文件夹最终不会出现在生成的映像中,但/root/.cache 会在所有后续构建中提供给 pip,这样你就能获得不错的速度!
NPM、Gradle 或其他软件包管理器也是如此。只需确保指定正确的目标文件夹即可。