Dockerfile最佳实践
Dockerfile包含一组指令集合。Docker镜像由每一条指令生成的只读层构成。镜像层是栈式的,并且每一层都基于上一层的变化。
FROM
创建一个基于ubuntu:18.04的镜像层COPY
将当前目录下面的文件拷贝到镜像中RUN
运行make指令构建应用CMD
指定容器需要运行的命令
当使用Docker镜像生成一个docker容器时,会在容器的最上层添加一个可写层,这个可写层称为容器层。在容器中对文件的增删改都会写入容器层。
构建上下文
docker镜像的构建上下文与Dockerfile文件的位置无关。如果在构建镜像时,Dockerfile正好处于构建上下文之中,且文件的名字为Dockerfile,则可以在构建镜像时省略Dockerfile的指定。
例如:
如果Dockerfile不在当前的构建上下文路径中,且Dockerfile文件的名字不为Dockerfile,则必须使用-f选项指定Dockerfile的路径和名称。
构建镜像时,不要将不需要的文件一起打包到镜像中,这些无用的文件增加镜像的大小以及镜像构建的速度。
当构建镜像时,终端会输出一条信息,提示当前构建镜像的构建上下文有多大。
通过管道来构建镜像
除了通过创建Dockerfile文件来构建镜像以外,我们也可以使用管道来临时构建镜像。
例如下面的示例:
使用下面的构建镜像的语法无需发送额外的文件给docker守护进程。-
让Docker从标准输入读取构建内容而不是目录。
下面的示例展示了如何通过标准输入的Dockerfile来构建一个镜像:
上面的构建方式不会有文件作为构建内容发送给Docker守护进程。
当我们不需要拷贝文件到镜像中时,可以使用上面这种方式。它可以提高构建速度,并且不会有文件发送给Docker守护进程。
使用.dockerignore文件来指定要排除的文件,从而减少构建镜像的大小。
当使用标准输入构建镜像不支持COPY或ADD指令。
使用来自标准输入的Dockerfile来构建一个使用本地构建上下文的镜像
使用下面这种语法可以实现这个目的:
下面的示例使用当前目录作为构建上下文,然后使用来自标准输入的Dockerfile构建镜像。
使用来自标准输入的Dockerfile来构建一个使用远端github仓库构建上下文的镜像
见下面示例:
使用远端git仓库构建镜像时,Docker会执行一个git clonemingl来将git仓库克隆到本地机器上,然后将仓库内容作为构建上下文发送给Docker守护进程。
使用这个特性需要确保本地机器安装了git命令。
使用.dockerignore文件来排除不需要的文件。文件支持的排除语法类似于.gitignore文件。
使用多阶段构建
由于镜像是在构建过程的最后阶段构建的,因此我们可以利用构建缓存最大限度地减少镜像层。
例如,如果我们的构建包含多个层,则可以将这些需要构建层根据更改频率的高低,从低到高排列,确保生成缓存可重复使用:
安装构建应用程序所需的工具
安装或更新库依赖关系
生成应用程序
Go应用程序的多阶段构建看起来像下面这样:
不要在构建镜像时安装不需要的包
解耦要构建的应用
每一个容器都应有保证职责的单一性。将应用解耦成多个不同的容器更容器水平扩展和复用容器。例如,一个应用栈可能由三个独立的容器构成,每一个容器都有自己独立的镜像。以解耦的方式来管理一个web应用,数据库和一个内存缓存。
限制容器只运行一个进程是好习惯,但是这并不是一成不变的规则。例如,一个容器可以spawn一个init进程,一些程序也会spawn多个进程。例如Apache可以为每一个请求创建一个进程。
根据自己的最佳判断,来尽可能保持容器的整洁和模块化。如果一个容器依赖其他容器,那么可以使用Docker网络来确保容器之间的通信。
最小化层的数量
在旧版本的Docker
中,我们必须最大限度地减少镜像中的镜像层数量,以确保它们是高效的。添加了以下功能以减少此限制:
只有
RUN,COPY,ADD
指令会创建层。其他指令创建临时的中间镜像层,它们不会增加镜像的大小。尽可能使用多阶段构建,只将需要的内容拷贝到最终的镜像中。我们可以在中间构建阶段来包含一些工具和调试信息,而不会增加最终镜像的大小。
使用按字母排序的多行参数
使用这种形式有助于使PR更加清晰易于阅读。也可以避免重复的包,使包列表的更新更容易。
见下面示例:
利用构建缓存
构建镜像时,Docker
会根据Dockerfile
中的指令按步骤和顺序执行。每一条指令都会被检查,Docker
会从它可以重复使用的缓存中查找一个已经存在的镜像,而不是创建一个重复的镜像。
如果不想使用缓存,可以在构建镜像时使用--no-cache=true
选项,如果想要让Docker使用自身的缓存,理解何时使用缓存,何时不能使用缓存非常重要。Docker
遵循以下基本规则:
从已处于缓存中的父镜像开始,将下一个指令与从该基础镜像中提取的所有子镜像进行比较,以查看其中一个指令是否使用完全相同的指令构建。如果没有,缓存将失效。
在大部分情况下,将
Dockerfile
中的一条指令与子镜像中的其中一个比较完全够了。然而,有些特定的指令需要更多的检查和解释。对于
ADD
和COPY
指令,将检查镜像中文件的内容,并为每个文件计算校验和。文件的上次修改和上次访问时间不会被计算进校验和中。在缓存查找过程中,校验和与现有镜像中的校验和进行比较。如果文件(如内容和元数据)中发生了任何更改,则缓存将失效。除了
ADD
和COPY
指令外,缓存检查不会查看容器中的文件来确定缓存是否匹配。例如,在处理RUN apt-get-y
更新命令时,不会检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只有命令字符串本身用于查找匹配项。
Dockerfile指令
FROM
无论何时,都尽可能使用官方镜像来作为基础镜像。可以使用Alpine镜像。
LABEL
在镜像中添加标签来组织和管理项目。对于每一个标签,添加一个以LABEL开头的行并且带有一对或多对键值对。下面的示例展示了不同的LABEL使用语法。
带有空格的字符串必须加上引号或者对空格进行转义。内部引号字符也必须进行转义。
一个镜像可以有一个或多个标签。可以在单个LABEL指令中指定多个标签。这样可以防止创建额外的层。
也可以使用下面这种形式
RUN
将复杂的或者很长的RUN语句使用反斜线分隔成多个独立的行来使Dockerfile易读,易理解,易维护。
在使用RUN指令时,可能会经常使用apt-get来安装软件包。使用apt-get安装软件包时,有一些需要注意的地方。
避免使用RUN apt-get upgrade和dist-upgrade,因为许多来自父镜像的软件包无法在一个非特权容器中进行升级。如果一个包含在父镜像中的软件包过期了,则需要联系镜像维护人员。如果需要更新一个特定的软件包foo,我们可以使用 apt-get install -y foo 来更新软件包。
在安装软件包时,总是应该在同一RUN指令中,使用apt-get update && apt-get install这样的组合。例如:
在单独的RUN语句行中使用apt-get update会导致缓存问题以及后续apt-get install指令的失败。例如:
构建完镜像以后,Docker会缓存所有的构建层。如果此时我们在修改了apt-get install来安装一个额外的软件包:
Docker将最初的和修改后的指令视为相同的指令,并重复使用以前步骤中的缓存。因此,由于构建镜像时使用缓存的版本,因此不会执行 apt-get update。由于apt-get update未运行,因此构建镜像时,可能会得到一个过时版本的curl和nginx软件包。
使用 RUN apt-get update && apt-get install - y 确保我们的Dockerfile安装最新的软件包版本,此技术称为"缓存破坏"
。我们还可以通过指定软件包版本来实现缓存破坏。例如:
版本固定迫使构建检索特定版本,无论缓存中的内容如何。此技术还可以减少由于所需软件包中的意外更改而出现的失败。
下面的例子展示了RUN指令的好的使用方式:
s3cmd指定了软件包的安装版本,因此会导致缓存破坏,从而始终安装最新的软件。在上面的示例中,我们将需要安装的软件包用单独的行列出来,一方面易于阅读和维护,另一方面可以防止重复指定安装相同的软件包。
安装完软件包以后,我们可以通过删除/var/lib/lists目录下的内容来减小镜像的大小。apt缓存不是保存在镜像层中的。由于RUN语句以apt-get update开头,因此,包缓存总是会在执行apt-get install之前被刷新。
ubuntu和debin发行版默认会自动执行apt-get clean来清理缓存,因此不需要明确调用这个命令。
使用管道
一些RUN指令依靠管道来将输出的结果传递给另外一个命令,使用管道符|来实现这个目的。
Docker使用/bin/sh -c解释器来执行这些命令,命令执行的成功与否取决于管道中最后一条命令执行的退出状态码。上面的例子中,即使wget命令执行失败了,此构建步骤也会成功并生成新的镜像。
如果希望命令在任何一个阶段出现错误都会失败,可以使用set -o pipefail && 来确保一个非预期的错误不会导致构建的意外成功。见下面示例:
并不是所有shell都支持 -o pipefail选项。在这种情况下,可以考虑使用RUN指令来明确运行一个支持次选项的shell。
例如 RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
CMD
CMD指令应用于运行镜像像中带有参数的软件。CMD应该始终使用CMD ["executable"、"param1"、"param 2"...]的形式。因此,如果镜像用于运行一个服务,如Apache和Rails,我么将运行类似CMD ["Apache2","-DFOREGROUND"]。事实上,对于任何基于服务的镜像,都建议使用这种形式的指令。
在大部分情况下,CMD应该被提供一个交互shell,例如bash,python和perl。例如,CMD ["perl", "de0"], CMD ["python"], 或者CMD ["php", "-a"]。使用这种形式意味着当我们执行某些命令例如docker run -it python时,我们将进入一个可用的shell终端。
CMD应该尽可能少的以 CMD["param1"、"param2"]的形式与ENTRYPOINT一起使用,除非用户已经非常熟悉ENTRYPOINT的工作原理。
EXPOSE
EXPOSE指令指示容器侦听连接的端口。因此,我们应该让我们的应用使用常见的传统端口。例如,包含 Apache Web 服务器的镜像将使用EXPOSE 80,而包含MongoDB的镜像将使用EXPOSE 27017等。
对于外部访问,用户可以执行带有flag的docker run,指示如何将指定端口映射到自己选择的端口。对于容器链接,Docker为从接收容器返回源的路径(即MYSQL_PORT_3306_TCP)提供环境变量。
ENV
可以使用ENV来为容器中安装的软件更新环境变量,ENV=/usr/local/nginx/bin:$PATH,确保CMD ["nginx"]能正常运行。
ENV也可以用于为需要容器化的应用提供需要的环境变量,例如Postgre’s PGDATA。
ENV还可以用于设置通过的版本号,以便版本号更容器维护,见下面的例子:
Dockerfile中设置的ENV变量值,也可以在命令行指定新的值来覆盖老的值,见示例: