ChatGPT解决这个技术问题 Extra ChatGPT

Dockerfile 中的多 RUN 与单链 RUN,哪个更好?

Dockerfile.1 执行多个 RUN

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 加入他们:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

每个 RUN 创建一个层,所以我总是假设层越少越好,因此 Dockerfile.2 更好。

RUN 删除前一个 RUN(即 yum install nano && yum clean all)添加的内容时,这显然是正确的,但在每个 RUN 添加内容的情况下,我们需要考虑以下几点:

层应该只是在前一层之上添加一个差异,所以如果后面的层不删除在前一层中添加的东西,那么两种方法之间不应该有太多的磁盘空间节省优势。层是从 Docker Hub 并行提取的,因此 Dockerfile.1 虽然可能稍大一些,但理论上下载速度会更快。如果添加第 4 句(即 echo This is the D > d)并在本地重建,由于缓存,Dockerfile.1 会更快地构建,但 Dockerfile.2 必须再次运行所有 4 个命令。

那么,问题来了:哪种方法更好地创建 Dockerfile?

一般无法回答,因为这取决于情况和图像的使用(针对大小、下载速度或构建速度进行优化)

B
BMitch

如果可能,我总是将创建文件的命令与删除这些相同文件的命令合并到一个 RUN 行中。这是因为每行 RUN 都会向图像添加一个层,输出实际上是您可以在它创建的临时容器上使用 docker diff 查看的文件系统更改。如果您删除在不同层创建的文件,联合文件系统所做的只是在新层中注册文件系统更改,该文件仍然存在于前一层中并通过网络传输并存储在磁盘上。所以如果你下载源代码,解压,编译成二进制文件,最后删除 tgz 和源文件,你真的希望这一切都在一个层中完成以减小图像大小。

接下来,我个人根据它们在其他图像中重用的潜力和预期的缓存使用情况来拆分图层。如果我有 4 个图像,所有图像都具有相同的基本图像(例如 debian),我可以将大部分图像的常用实用程序集合拉到第一个运行命令中,以便其他图像受益于缓存。

在查看图像缓存重用时,Dockerfile 中的顺序很重要。我会查看很少更新的任何组件,可能只有在基础映像更新并将这些组件放在 Dockerfile 中时。在 Dockerfile 的末尾,我包含了任何可以快速运行并且可能经常更改的命令,例如添加具有主机特定 UID 的用户或创建文件夹和更改权限。如果容器包含正在积极开发的解释代码(例如 JavaScript),则会尽可能晚地添加,以便重建仅运行该单个更改。

在每组更改中,我都尽可能地整合以最小化层级。因此,如果有 4 个不同的源代码文件夹,它们将被放置在一个文件夹中,以便可以使用单个命令添加它。任何从类似 apt-get 安装的软件包都会在可能的情况下合并到一个 RUN 中,以最大限度地减少软件包管理器的开销(更新和清理)。

多阶段构建的更新:

我不太担心在多阶段构建的非最终阶段减少图像大小。当这些阶段没有被标记并传送到其他节点时,您可以通过将每个命令拆分到单独的 RUN 行来最大限度地提高缓存重用的可能性。

但是,这不是压缩层的完美解决方案,因为您在阶段之间复制的只是文件,而不是图像元数据的其余部分,如环境变量设置、入口点和命令。而且,当您在 linux 发行版中安装软件包时,库和其他依赖项可能会分散在整个文件系统中,从而难以复制所有依赖项。

因此,我使用多阶段构建来替代在 CI/CD 服务器上构建二进制文件,这样我的 CI/CD 服务器只需要运行 docker build 的工具,而不需要 jdk、nodejs、 go,并安装任何其他编译工具。


C
Community

他们的最佳实践中列出的官方答案(官方图片必须遵守这些)

最小化层数您需要在 Dockerfile 的可读性(以及长期可维护性)和最小化它使用的层数之间找到平衡。对您使用的层数保持战略性和谨慎。

从 docker 1.10 开始,COPYADDRUN 语句为您的图像添加了一个新层。使用这些语句时要小心。尝试将命令组合成一个 RUN 语句。仅在需要可读性时才将其分开。

更多信息:https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

更新:docker 中的多阶段 >17.05

通过多阶段构建,您可以在 Dockerfile 中使用多个 FROM 语句。每个 FROM 语句都是一个阶段,可以有自己的基本映像。在最后阶段,您使用像 alpine 这样的最小基础镜像,从之前的阶段复制构建工件并安装运行时要求。这个阶段的最终结果就是你的形象。所以这就是你担心前面描述的层的地方。

像往常一样,docker 在多阶段构建中有 great docs。这是一个简短的摘录:

对于多阶段构建,您可以在 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的基础,并且它们中的每一个都开始构建的新阶段。您可以选择性地将工件从一个阶段复制到另一个阶段,从而在最终图像中留下您不想要的一切。

可以在此处找到一篇关于此的精彩博文:https://blog.alexellis.io/mutli-stage-docker-builds/

回答你的观点:

是的,图层有点像差异。如果变化绝对为零,我认为不会添加层。问题是,一旦您在第 2 层安装/下载了某些内容,就无法在第 3 层删除它。因此,一旦将某些内容写入图层,就不能再通过删除它来减小图像大小。尽管可以并行拉取层,使其可能更快,但每一层无疑都会增加图像大小,即使它们正在删除文件。是的,如果您要更新 docker 文件,缓存会很有用。但它在一个方向上起作用。如果您有 10 层,并且您更改了第 6 层,您仍然需要重建第 6 层到第 10 层的所有内容。因此,它不会经常加快构建过程,但可以保证不必要地增加图像的大小。

感谢 @Mohan 提醒我更新此答案。


这现在已经过时了 - 请参阅下面的答案。
@Mohan 谢谢提醒!我更新了帖子以帮助用户。
J
Jaroslav Bezděk

上面的答案似乎已经过时了。文档注释 this

在 Docker 17.05 之前,甚至更多,在 Docker 1.10 之前,尽量减少镜像中的层数非常重要。以下改进缓解了这种需求:[...] Docker 17.05 及更高版本添加了对多阶段构建的支持,允许您仅将所需的工件复制到最终映像中。这允许您在中间构建阶段包含工具和调试信息,而不会增加最终映像的大小。

this

请注意,此示例还使用 Bash && 运算符人为地将两个 RUN 命令压缩在一起,以避免在图像中创建额外的层。这是容易失败且难以维护的。

最佳做法似乎已更改为使用多阶段构建并保持 Dockerfile 的可读性。


虽然多阶段构建似乎是保持平衡的好选择,但当 docker image build --squash 选项超出实验范围时,才能真正解决这个问题。
@Yajo - 我对 squash 能否通过实验持怀疑态度。它有很多噱头,只有在多阶段构建之前才有意义。使用多阶段构建,您只需要优化最后阶段,这非常容易。
@Yajo 对此进行扩展,只有最后阶段的图层对最终图像的大小有任何影响。因此,如果您将所有构建器 gubbins 放在早期阶段,并在最后阶段安装包并复制早期阶段的文件,那么一切都很好,并且不需要 squash。
与您的开场白相反,“以上答案已过时”其他答案仍然正确。您选择性地引用文档,并假设每个人都会切换到多阶段构建来获得性能改进。虽然多阶段构建很棒,但它们可能不是每个人的最佳解决方案。
J
Jaroslav Bezděk

这取决于您在图像层中包含的内容。关键是共享尽可能多的层。

不好的例子

Dockerfile RUN yum install big-package && yum install package1 Dockerfile RUN yum install big-package && yum install package2

好例子

Dockerfile RUN yum install big-package RUN yum install package1 Dockerfile RUN yum install big-package RUN yum install package2

另一个建议是删除只有在与添加/安装操作发生在同一层时才那么有用。


这两个真的会共享缓存中的 RUN yum install big-package 吗?
是的,只要它们从同一个基础开始,它们将共享同一层。