你肯定你会写 Dockerfile 吗?

原文连接: 你肯定你会写 Dockerfile 吗?

现在 GitHub 仓库中已经包含了成千上万的 Dockerfile,但并非全部的 Dockerfile 都是高效的。本文将从五个方面来介绍 Dockerfile 的最佳实践,以此来帮助你们编写更优雅的 Dockerfile。若是你是 Docker 的初学者,恭喜你,这篇文章就是为你准备的。后面的系列将会更加深刻,敬请期待!docker

本文使用一个基于 Maven 的 Java 项目做为示例,而后不断改进 Dockerfile 的写法,直到最后写出一个最优雅的 Dockerfile。中间的全部步骤都是为了说明某一方面的最佳实践。

1. 减小构建时间

一个开发周期包括构建 Docker 镜像,更改代码,而后从新构建 Docker 镜像。在构建镜像的过程当中,若是可以利用缓存,能够减小没必要要的重复构建步骤。缓存

构建顺序影响缓存的利用率

镜像的构建顺序很重要,当你向 Dockerfile 中添加文件,或者修改其中的某一行时,那一部分的缓存就会失效,该缓存的后续步骤都会中断,须要从新构建。因此优化缓存的最佳方法是把不须要常常更改的行放到最前面,更改最频繁的行放到最后面。安全

只拷贝须要的文件,防止缓存溢出

当拷贝文件到镜像中时,尽可能只拷贝须要的文件,切忌使用 COPY . 指令拷贝整个目录。若是被拷贝的文件内容发生了更改,缓存就会被破坏。在上面的示例中,镜像中只须要构建好的 jar 包,所以只须要拷贝这个文件就好了,这样即便其余不相关的文件发生了更改也不会影响缓存。maven

最小化可缓存的执行层

每个 RUN 指令都会被看做是可缓存的执行单元。太多的 RUN 指令会增长镜像的层数,增大镜像体积,而将全部的命令都放到同一个 RUN 指令中又会破坏缓存,从而延缓开发周期。当使用包管理器安装软件时,通常都会先更新软件索引信息,而后再安装软件。推荐将更新索引和安装软件放在同一个 RUN 指令中,这样能够造成一个可缓存的执行单元,不然你可能会安装旧的软件包。ide

2. 减少镜像体积

镜像的体积很重要,由于镜像越小,部署的速度更快,攻击范围越小。工具

删除没必要要依赖

删除没必要要的依赖,不要安装调试工具。若是实在须要调试工具,能够在容器运行以后再安装。某些包管理工具(如 apt)除了安装用户指定的包以外,还会安装推荐的包,这会平白无故增长镜像的体积。apt 能够经过添加参数 -–no-install-recommends 来确保不会安装不须要的依赖项。若是确实须要某些依赖项,请在后面手动添加。post

删除包管理工具的缓存

包管理工具会维护本身的缓存,这些缓存会保留在镜像文件中,推荐的处理方法是在每个 RUN 指令的末尾删除缓存。若是你在下一条指令中删除缓存,不会减少镜像的体积。优化

固然了,还有其余更高级的方法能够用来减少镜像体积,以下文将会介绍的多阶段构建。接下来咱们将探讨如何优化 Dockerfile 的可维护性、安全性和可重复性。ui

3. 可维护性

尽可能使用官方镜像

使用官方镜像能够节省大量的维护时间,由于官方镜像的全部安装步骤都使用了最佳实践。若是你有多个项目,能够共享这些镜像层,由于他们均可以使用相同的基础镜像。spa

使用更具体的标签

基础镜像尽可能不要使用 latest 标签。虽然这很方便,但随着时间的推移,latest 镜像可能会发生重大变化。所以在 Dockerfile 中最好指定基础镜像的具体标签。咱们使用 openjdk 做为示例,指定标签为 8。其余更多标签请查看官方仓库

使用体积最小的基础镜像

基础镜像的标签风格不一样,镜像体积就会不一样。slim 风格的镜像是基于 Debian 发行版制做的,而 alpine 风格的镜像是基于体积更小的 Alpine Linux 发行版制做的。其中一个明显的区别是:Debian 使用的是 GNU 项目所实现的 C 语言标准库,而 Alpine 使用的是 Musl C 标准库,它被设计用来替代 GNU C 标准库(glibc)的替代品,用于嵌入式操做系统和移动设备。所以使用 Alpine 在某些状况下会遇到兼容性问题。 以 openjdk 为例,jre 风格的镜像只包含 Java 运行时,不包含 SDK,这么作也能够大大减小镜像体积。

4. 重复利用

到目前为止,咱们一直都在假设你的 jar 包是在主机上构建的,这还不是理想方案,由于没有充分利用容器提供的一致性环境。例如,若是你的 Java 应用依赖于某一个特定的操做系统的库,就可能会出现问题,由于环境不一致(具体取决于构建 jar 包的机器)。

在一致的环境中从源代码构建

源代码是你构建 Docker 镜像的最终来源,Dockerfile 里面只提供了构建步骤。

首先应该肯定构建应用所需的全部依赖,本文的示例 Java 应用很简单,只须要 MavenJDK,因此基础镜像应该选择官方的体积最小的 maven 镜像,该镜像也包含了 JDK。若是你须要安装更多依赖,能够在 RUN 指令中添加。pom.xml 文件和 src 文件夹须要被复制到镜像中,由于最后执行 mvn package 命令(-e 参数用来显示错误,-B 参数表示以非交互式的“批处理”模式运行)打包的时候会用到这些依赖文件。

虽然如今咱们解决了环境不一致的问题,但还有另一个问题:每次代码更改以后,都要从新获取一遍 pom.xml 中描述的全部依赖项。下面咱们来解决这个问题。

在单独的步骤中获取依赖项

结合前面提到的缓存机制,咱们可让获取依赖项这一步变成可缓存单元,只要 pom.xml 文件的内容没有变化,不管代码如何更改,都不会破坏这一层的缓存。上图中两个 COPY 指令中间的 RUN 指令用来告诉 Maven 只获取依赖项。

如今又遇到了一个新问题:跟以前直接拷贝 jar 包相比,镜像体积变得更大了,由于它包含了不少运行应用时不须要的构建依赖项。

使用多阶段构建来删除构建时的依赖项

多阶段构建能够由多个 FROM 指令识别,每个 FROM 语句表示一个新的构建阶段,阶段名称能够用 AS 参数指定。本例中指定第一阶段的名称为 builder,它能够被第二阶段直接引用。两个阶段环境一致,而且第一阶段包含全部构建依赖项。

第二阶段是构建最终镜像的最后阶段,它将包括应用运行时的全部必要条件,本例是基于 Alpine 的最小 JRE 镜像。上一个构建阶段虽然会有大量的缓存,但不会出如今第二阶段中。为了将构建好的 jar 包添加到最终的镜像中,可使用 COPY --from=STAGE_NAME 指令,其中 STAGE_NAME 是上一构建阶段的名称。

多阶段构建是删除构建依赖的首选方案。

本文从在非一致性环境中构建体积较大的镜像开始优化,一直优化到在一致性环境中构建最小镜像,同时充分利用了缓存机制。下一篇文章将会介绍多阶段构建的更多其余用途。


相关文章
相关标签/搜索