Dockerfile编写指南
镜像是容器运行的基础,直接使用镜像库的镜像虽然可以满足一定需求,但通常针对自己的项目还是需要手动定制镜像,这就是Dockerfile的功能了。本文总结一下编写Dockerfile的一些注意事项。
指令介绍
Dockerfile本身是一个文本文件,其定制镜像的流程就是执行内部的指令,每条指令构建一层,最终获取定制镜像。因此在讨论最佳实践之前,有必要简单了解下各个指令的意义。
MAINTAINER
该指令用来声明作者,推荐放置在Dockerfile的起始部分。
LABEL
给镜像添加标签,同上一条指令一样,给镜像增加一些说明信息。
FROM
这可能是最重要的指令了,定制镜像必然是从一个基础镜像拓展得来,可以类比于类的继承,从上到下依次有服务类镜像nginx
,mysql
等,应用类镜像node
,python
等,系统类镜像ubuntu
,debian
等,甚至空白镜像scratch
。基础镜像基本涵盖了所有场景。通常我们会根据自身应用场景寻找一个最合适的镜像进行定制,该命令必须是Dockerfile的第一条指令。
RUN
该指令用来执行定制过程中的命令行命令,应该是最常用的指令。其执行格式有两种如下。
- shell: 在命令行中直接输入命令的格式
- exec: 类似于函数调用,格式为['可执行文件','参数',...]
RUN的行为实际上就是在当前镜像生成容器的外层执行命令并commit一个新的镜像层作为下一指令的基础。
COPY
该命令用于将构建上下文目录中的目标文件复制到镜像内的目标位置。说到这里,就要先了解一下构建上下文的概念。
首先要知道docker在运行时是分成两个部分的,一部分为Docker引擎运行在服务端,一部分为客户端工具运行在客户端。客户端工具通过Docker引擎提供的API完成各种功能交互,所以我们在本机执行的各种docker操作实际上都是通过远程调用的形式在服务端进行的。那么服务端是怎么获取运行时所需要的本地文件的呢?这就需要构建上下文了。当构建时,用户会指定构建上下文路径,客户端会将该路径下的所有文件打包并上传到Docker引擎供服务端使用。通常Dockerfile存放的位置就是构建上下文。
源路径可以是多个,目标路径可以是容器内的绝对路径或者相对于工作目录(用WORKDIR指令指定)的相对路径。目标路径的目录如果不存在会在复制文件前先创建目录。
Add
该指令的用法和COPY完全相同,但在其上做了一些功能拓展如下。
- 源路径可以是URL,这种情况下Docker会去下载该URL文件COPY到目标路径。
- 原路径获取文件为
tar
压缩文件时,会自动解压缩到目标路径。
CMD
在介绍这个指令前需要回顾一下容器的本质,应该知道Docker和虚拟机的区别之一就在于其单元是在进程级别的,启动容器实际上就是启动一个进程,那么就需要指定该进程所运行的程序和参数。CMD命令就是用来指定容器进程的启动命令。简单来说,RUN指令会在镜像构建时执行,当通过镜像构建完成容器,需要将这个容器进程启动时,就会执行CMD指令。格式分为如下三类。
- shell:CMD <命令>。实际执行时会被包装为
sh -c
的形式执行。 - exec:CMD ["可执行文件", "参数", ...]。
- 参数列表:CMD ["参数", ...],指定了ENTRYPOINT后使用该格式。
ENTRYPOINT
该命令用于给容器配置一个可执行命令,即每次创建容器时,可以设置一个特定的应用程序为默认启动程序。目的其实和CMD类似,都是为了在容器启动时执行一些操作。当执行了该命令后,CMD就不再具体运行命令,而是将参数传递给该命令指定的应用。常规场景应用有如下两种。
- 让镜像变的像命令一样灵活传递参数使用
- 容器运行前执行一些预处理工作
ENV
设置环境变量供后续指令或容器运行时使用,格式为键值对形式:ENV NAME=VALUE
ARG
构建参数。和环境变量类似,不过区别在于该变量只能在构建时使用,运行时不会存在。
VOLUME
介绍这个指令之前需要简单了解下数据卷的概念。先想象一个场景,当启动容器后进行一些数据修改操作后关闭容器,再重新启动容器时这些数据状态还保存着吗?实际上当重新开启容器时,内部会恢复到镜像的初始化状态,数据无法做到持久化。
为了解决这个问题,DOCKER提倡容器运行时不要在存储层发生写操作,而是将数据文件保存到VOLUME(数据卷)中单独管理。数据卷是绕过联合文件系统的特别指定目录,独立于容器的生命周期,因此更新或删除容器时,数据卷中的数据仍然保留。该指令用于将指定目录挂载为匿名数据卷,容器启动后任何向匿名数据卷写入的信息都不会记录到容器存储层,从而实现数据持久化。
EXPOSE
该指令声明运行时容器提供的服务端口,需要注意这只是一个声明,并不会执行开启这个端口的服务,主要用途在于帮助使用者理解这个镜像的守护端口方便映射。
WORKDIR
指定工作目录用于给RUN,CMD,ENTRYPOINT使用,默认为根目录。
USER
切换用户身份,和WORKDIR类似,都会改变环境状态影响到后续指令的镜像层。
HEALTHCHECK
自定义命令判断容器状态是否正常。默认情况下,Docker引擎只能通过容器进程是否退出来判断容器状态,但有时候会出现的情况是程序进入死循环等情况时,服务异常了但是进程并不退出,这是Docker无法判断的。该指定给予了我们自定义检查的能力。
ONBUILD
该指令在当前镜像被构建时不会执行,而是被当做基础镜像引用,构建拓展镜像时才会执行,其参数为其他指令。
最佳实践建议
建议分为两个部分,一部分针对总体原则,一部分针对指令具体使用上的原则。
总体原则
- 容器轻量化,对于每个容器的创建和销毁应该尽可能简单
- 使用.dockerignore文件排除不需要的文件,仅添加构建所需文件
- 仅安装构建所需安装包
- 将应用程序解耦到多个容器,保证每个容器职责的单一
- 在保持可读性的前提下减少镜像层数
- 将多行参数按字母表顺序排序并在反斜杠符号前增加空格,以增加可读性
- 尽可能的使用构建缓存
指令使用原则
- 尽可能的使用官方镜像库作为FROM指令的基础
- 保持可读性的前提下,使用一个LABEL指令记录尽可能多的标签
- 同样的尽可能的压缩多个步骤到一个RUN命令,并合理利用反斜线分割为多行
- 使用RUN命令时,不要使用
apt-get upgrade
或dist-upgrade
,这会更新大量非必要系统包,使用apt-get install -y
代替,并且要保证和apt-get update
组合使用防止缓存镜像带来的包版本过时的问题。安装完成后清理apt缓存减少镜像大小。 - 任何服务型镜像都尽可能使用exec形式的CMD命令
- EXPOSE尽可能声明相应服务的常见端口
- 使用ENV为容器中的服务提供必要的环境变量,也可以用来设置常见版本号
- 相比于ADD优先使用COPY保证指令职责的单一,除非是向镜像内添加压缩包。添加远程文件推荐使用RUN命令执行
wget
或curl
- 当需要持久化数据时,必须使用VOLUME为用户创建数据卷
- 如果镜像服务不需要授权,尽可能使用USER指令切换到非root用户
- 切换工作目录时只使用WORKDIR而不用cd指令,保证可读性
- 如果有使用ONBUILD指令,打标签时应该添加onbuild信息
-- EOF --