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?
如果可能,我总是将创建文件的命令与删除这些相同文件的命令合并到一个 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,并安装任何其他编译工具。
他们的最佳实践中列出的官方答案(官方图片必须遵守这些)
最小化层数您需要在 Dockerfile 的可读性(以及长期可维护性)和最小化它使用的层数之间找到平衡。对您使用的层数保持战略性和谨慎。
从 docker 1.10 开始,COPY
、ADD
和 RUN
语句为您的图像添加了一个新层。使用这些语句时要小心。尝试将命令组合成一个 RUN
语句。仅在需要可读性时才将其分开。
更新: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 提醒我更新此答案。
上面的答案似乎已经过时了。文档注释 this:
在 Docker 17.05 之前,甚至更多,在 Docker 1.10 之前,尽量减少镜像中的层数非常重要。以下改进缓解了这种需求:[...] Docker 17.05 及更高版本添加了对多阶段构建的支持,允许您仅将所需的工件复制到最终映像中。这允许您在中间构建阶段包含工具和调试信息,而不会增加最终映像的大小。
和this:
请注意,此示例还使用 Bash && 运算符人为地将两个 RUN 命令压缩在一起,以避免在图像中创建额外的层。这是容易失败且难以维护的。
最佳做法似乎已更改为使用多阶段构建并保持 Dockerfile
的可读性。
docker image build --squash
选项超出实验范围时,才能真正解决这个问题。
squash
能否通过实验持怀疑态度。它有很多噱头,只有在多阶段构建之前才有意义。使用多阶段构建,您只需要优化最后阶段,这非常容易。
这取决于您在图像层中包含的内容。关键是共享尽可能多的层。
不好的例子
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
吗?