Dockerfile

Dockerfile中的注释行,都是以#开头的。

除注释之外,每一行都是一条指令(Instruction)。指令的格式是指令参数如下。

1
INSTRUCTION argument

指令是不区分大小写的,但是通常都采用大写的方式。这样Dockerfile的可读性会高一些。

Docker image build 命令会按行来解析Dockerfile中的指令并顺序执行。

部分指令会在镜像中创建新的镜像层,其他指令只会增加或修改镜像的元数据信息。

新增镜像层的指令包括FROMRUN 以及COPY等 ,而新增元数据的指令包括EXPOSEWORKDIRENV 以及ENTERPOINT 等。关于如何区分命令是否会新建镜像层,一个基本的原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果只是告诉Docker如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。

可以通过docker image history 来查看在构建镜像的过程中都执行了哪些指令。

多阶段构建

对于Docker镜像来说,过大的体积并不好!

越大则越慢,这就意味着更难使用,而且可能更加脆弱,更容易遭受攻击。

鉴于此,Docker镜像应该尽量小。对于生产环境镜像来说,目标是将其缩小到仅包含运行应用所必需 的内容即可。

例如,不同的Dockerfile写法就会对镜像的大小产生显著影响。常见的例子是,每一个RUN指令会新增一个镜像层。因此,通过使用&& 连接多个命令以及使用反斜杠(\ )换行的方法,将多个命令包含在一个RUN指令中,通常来说是一种值得提倡的方式。

多阶段构建能够在不增加复杂性的情况下优化构建过程!

多阶段构建方式使用一个Dockerfile,其中包含多个FROM 指令。每一个FROM 指令都是一个新的构建阶段(Build Stage) ,并且可以方便地复制之前阶段的构件。

最佳实践

利用缓存构建

Docker的构建过程利用了缓存机制。观察缓存效果的一个方法,就是在一个干净的Docker主机上构建一个新的镜像,然后再重复同样的构建。第一次构建会拉取基础镜像,并构建镜像层,构建过程需要花费一定时间;第二次构建几乎能够立即完成。这就是因为第一次构建的内容(如镜像层)能够被缓存下来,并被后续的构建过程复用。

docker image build 命令会从顶层开始解析Dockerfile中的指令并逐行执行。而对每一条指令,Docker都会检查缓存中是否已经有与该指令对应的镜像层。如果有,即为缓存命中(Cache Hit),并且会使用这个镜像层;如果没有,则是缓存未命中(Cache Miss),Docker会基于该指令构建新的镜像层。缓存命中能够显著加快构建过程。

如果找到该镜像层,Docker会跳过这条指令,并链接到这个已经存在的镜像层,然后继续构建;如果无法找到符合要求的镜像层,则设置缓存无效并构建该镜像层。此处“设置缓存无效”作用于本次构建的后续部分。也就是说Dockerfile中接下来的指令将全部执行而不会再尝试查找构建缓存。

理解以下两点很重要:

首先,一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写Dockerfile时须特别注意这一点,尽量将易于发生变化的指令置于Dockerfile文件的后方执行。这意味着缓存未命中的情况将直到构建的后期才会出现——从而构建过程能够尽量从缓存中获益。通过对docker image build 命令加入--nocache=true参数可以强制忽略对缓存的使用。

还有一点也很重要,那就是COPYADD 指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化。例如,有可能Dockerfile中的COPY . /src 指令没有发生变化,但是被复制的目录中的内容已经发生变化了。为了应对这一问题,Docker会计算每一个被复制文件的Checksum值,并与缓存镜像层中同一文件的checksum进行对比。如果不匹配,那么就认为缓存无效并构建新的镜像层。

合并镜像

合并镜像并非一个最佳实践,因为这种方式利弊参半。

当镜像中层数太多时,合并是一个不错的优化方式。例如,当创建一个新的基础镜像,以便基于它来构建其他镜像的时候,这个基础镜像就最好被合并为一层。

缺点是,合并的镜像将无法共享镜像层。这会导致存储空间的低效利用,而且push和pull操作的镜像体积更大。

执行docker image build 命令时,可以通过增加--squash参数来创建一个合并的镜像。

使用no-install-recommends

在构建Linux镜像时,若使用的是APT包管理器,则应该在执行apt-get install 命令时增加no-install-recommends 参数。这能够确保APT仅安装核心依赖(Depends 中定义)包,而不是推荐和建议的包。这样能够显著减少不必要包的下载数量。