什么是 Dockerfile


Dockerfile 是一个文本文件,里面包含了打包Docker 镜像所需要用到的命令。Docker 可以通过读取 Dockerfile 里面的命令来自动化地构建 Docker 镜像。通过执行 docker build 就可以启动这样的一个自动化流程。


编写 Dockerfile 的五个最佳实践_java


容器镜像层


Docker 镜像由只读层组成,每个层都代表一个 Dockerfile 指令,这些层是堆叠的,每一层都是前一层变化的增量。运行镜像并生成容器时,可以在基础镜像的顶部添加新的可写层("容器层")。对正在运行的容器所做的所有更改(例如写入新文件,修改现有文件和删除文件)都将写入此可写容器层。


编写 Dockerfile 的五个最佳实践_java_02


了解 Docker Build 上下文(Context)


Docker build 有一个重要参数 context,默认是当前目录,采用.代替,但为了避免将不必要的文件打包到镜像,造成 context 及镜像大小过大,也可以用指定的目录作为 context。context 过大会造成 docker build 很耗时,镜像过大则会造成 docker pull/push 性能变差以及运行时容器体积过大浪费空间资源。


对于 Docker17.05及以上版本,有一个 remote context 的特性,即可以将远程资源作为 context,如下所示:


编写 Dockerfile 的五个最佳实践_java_03


分阶段构建


构建镜像最大的挑战莫过于防止镜像过大,造成实际运行时由于并发而导致拉取性能问题。为了应对这个挑战,很多转型到容器的团队采用两个 Dockerfile,一个负责开发环境的镜像构建,一个负责生产环境的镜像构建。开发镜像包含了代码构建所需要的环境,镜像大小自然比较大,生产镜像仅包含应用运行所需要的内容,是很精简的体积很小的镜像。


一个开发镜像 Dockerfile 示例如下:


编写 Dockerfile 的五个最佳实践_java_04


一个生产镜像 Dockerfile 示例如下:


编写 Dockerfile 的五个最佳实践_java_05


这时候整个过程分为三步:


1.根据开发镜像启动一个容器来构建 app

2. 构建结束后的 app二进制拷贝到主机 

3. 根据生产镜像启动一个容器,将主机上的 app 二进制 copy 到生产镜像中。如下脚本可自动化执行该过程。


编写 Dockerfile 的五个最佳实践_java_06


这么做会大量占用主机的空间资源,并且运行一段时间后,遗留很多二进制 app 文件在主机上,后期清理还需要额外的维护工作,显然不是一个很好的实践。


Docker 分阶段构建就可以很好地解决这个问题,使用如下的 Dockerfile 即可:


编写 Dockerfile 的五个最佳实践_java_07


整个 Dockerfile 流程很清晰,也不在需要额外的 shell 脚本来支持整个流程,并且我们可以指定执行的 stage,具体命令如下:


编写 Dockerfile 的五个最佳实践_java_08


使用构建缓存


构建映像时,Docker 会逐步按指定的顺序执行 Dockerfile 中的每个指令。在检查每条指令时,Docker 会在其缓存中查找可以重用的现有镜像,而不是重复创建新的镜像。


如果不想使用缓存,可以在 docker build 时加上--no-cache=true参数。然后,使用缓存会使得整个构建过程更高效,因此了解什么样的情形下可以利用缓存来提升效率很重要。具体来说有下面几个情形需要了解:


  • 从已经在缓存中的父镜像开始,将下一条指令与从该基本镜像导出的所有子镜像进行比较,以查看它们中的一个是否使用完全相同的指令构建。如果不是,则缓存无效。

  • 在大多数情况下,只需将 Dockerfile 中的指令与其中一个子镜像进行比较即可。但是,某些 Dockerfile 命令需要更深入的检查。对于 ADD 和 COPY 指令,将检查镜像中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改时间和最后访问时间。在缓存查找期间,将校验和与现有映像中的校验和进行比较。如果文件中有任何更改(例如内容和元数据),则缓存无效。

  • 除了 ADD 和 COPY 命令之外,缓存检查不会查看容器中的文件以确定缓存匹配。例如,在处理 RUN apt-get -y update 命令时,不检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配项。


给镜像设置标签(Label)


每一个对象都包含相应的元数据信息,比如 Docker 镜像就包含作者、大小等等元数据信息。在使用镜像的时候,往往会通过这些元数据信息来查找适合的镜像来用于开发或测试,而不单单只是通过名字去检索。


在 Dockerfile 中可以使用 Label 命令来为镜像增加 Label,示例如下:


编写 Dockerfile 的五个最佳实践_java_09


我们可以查看到 Label 及使用 Label 进行筛选:


编写 Dockerfile 的五个最佳实践_java_10


CMD 和 ENTRYPOINT 指令结合


官方关于 CMD 和 ENTRYPOINT 指令的说明如下


编写 Dockerfile 的五个最佳实践_java_11


简而言之,就是 CMD 提供运行时的动态覆盖参数机制,而 ENTRYPOINT 只是容器启动时的执行入口。


假设有如下 Dockerfile:


编写 Dockerfile 的五个最佳实践_java_12

当不指定任何参数时,情形如下:


编写 Dockerfile 的五个最佳实践_java_13


当指定一个ip地址,如192.168.137.4时,情况如下:


编写 Dockerfile 的五个最佳实践_java_14


这种机制意味着更好的可移植性,用户可在执行时动态注入变量,如当前环境类型、认证信息等等,便于服务的迁移、扩容等场景。


总结


Dockerfile 在实际基于 docker 的开发中使用非常普遍,很多开发者都掌握了基本的编写技巧,但对于一些优化的策略、方法掌握比较少,那么以上这些实践能够帮助大家节省时间、提升效率和性能,甚至提升应用的移植灵活性等。