Dockerfile指令

WORKDIR

格式为 WORKDIR <工作路径>

该指令用来指定工作目录,如果目录不存在会自动创建。以后各层的指令都会基于该工作路径执行。

如果未指定,则默认的工作目录为/

WORKDIR指令可以使用相对路径,也可以使用绝对路径。当提供相对路径时,它将向相对于上一条指令的WORKDIR路径解析。

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
# 这里最终的输出是/a/b/c

WORKDIR 指令可以解析环境变量ENV,例如:

ENV DIRPATH=/path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd
# 这里最终的输出是/path/$DIRNAME

ENV

格式有两种:

  • ENV <key> <value>

  • ENV <key1>=<value1> <key2>=<value2>

无论是后面其它指令,还是运行时的应用,都可以在这里定义环境变量。

定义了环境变量,在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile 中,就有类似的代码:

这里预先定义了环境变量 NODE_VERSION,其后的 RUN 这层,多次使用 $NODE_VERSION 来进行操作定制。将来在升级版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

环境变量可以使用到的地方很多,通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需要使用不同的环境变量即可。

ARG

格式:ARG <参数>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量,不同的是,ARG 所设置的是构建环境的环境变量,将来在容器运行时是不存在这些环境变量的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

使用上述 Dockerfile 会发现无法输出 ${DOCKER_USERNAME}变量的值,想要正常输出,必须在 FROM 之后再次指定 ARG

对于多阶段构建,尤其要注意这个问题

上述 Dockerfile 中两个 FROM 指令都可以使用 ${DOCKER_USERNAME},对于在各个阶段中使用的变量都必须在各个阶段分别指定。

COPY

格式:

  • COPY [--chown=<user>:<group>] <源路径> <目标路径>

  • COPY [--chown=<user>:<group>] ["<源路径1>", "<目标路径>"]

COPY 指令将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像的<目标路径>位置。

<源路径>可以是多个,甚至可以是通配符,其通配符要满足 Go 的filepath.Match 规则,如:

<目标路径>可以是容器内的绝对路径,也可以是相对于工作区的相对路径。目标路径不需要事先创建,如果目录不存在会在复制文件前自动创建确实目录。

使用COPY指令,源文件的各种源数据都会保留。比如读、写、执行全县、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

在使用该指令的时候还可以加上--chown=<user>:<group>选项来改变文件的所属用户及所属组。

如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

ADD

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个连接的文件放到<目标路径>去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,呢么还需要增加额外一层RUN进行调整,另外如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层RUN指令进行解压缩。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令会自动解压缩到 <目标路径> 中。

在 Docker 官方的 Dockerfile最佳实践文档中要求,尽可能的使用 COPY,因为 COPY 的语意明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场景,就是需要自动解压缩的场合 。

需要注意的是,ADD 指令会令构建缓存失效,从而可能会令镜像构建变得比较缓慢。

VOLUME

格式:

  • VOLUME ["路径1", "路径2"]

  • VOLUME 路径

容器咋运行时应尽量保持容器存储层不发生写操作,对于类似数据库需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存为卷,在Dockerfile中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

这里的/data目录会在容器运行时自动挂载为匿名卷,任何向/data中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态变化。

运行容器是也可以手动挂载,它会覆盖匿名挂载。

EXPOSE

该指令指定 Docker 容器在运行时监听的网络端口。你可以指定监听TCP还是UDP,如果不指定协议,则默认监听TCP

EXPOSE 指令并不发布端口,这里只是声明容器在运行时使用什么端口访问。想要在运行时公开端口,使用docker run命令配合-p选项来公开映射一个或多个端口,或者使用-P标志来映射所有公开的端口。

默认情况下,EXPOSE 监听 TCP,你还可以指定UDP:

此时如果你想映射公开的端口到你的宿主机上:

如果想同时公开TCP和UDP,需要同时添加两种协议的端口声明:

CMD

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell格式:CMD <命令>

  • exec格式:CMD ["可执行文件", "参数1", "参数2"]

  • 参数列表格式:CMD ["参数1", "参数2" ...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来代替镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD 是 /bin/bash ,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/ps-release。这就是用cat /etc/os-release 命令替换了默认的 /bin/bash 命令。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

在实际执行中,会将其变更为:

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

Docker 不是虚拟机,容器中的应用都应该以前台的方式执行,而不是向虚拟机、物理机那样,用 systemd 去启动后台服务,容器内没有后台服务的概念。

一些初学者将CMD写为:

然后发现容器执行后就立即退出了,甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异。

对于容器而言,启动程序就是容器应用进程,容器局势为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

ENTPYPOINT

ENTRYOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是指定容器启动程序及其参数。ENTRYPOINT 在运行时可以替代,不过要比 CMD 略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行命令,而是将 CMD 的内容作为参数传递给 ENTRYPOITN 指令。实际执行时,将变成:

为什么有了 CMD 后还要有 ENTRYPOINT 呢?ENTRYPOINT 到底有什么好处呢?让我们来看几个场景。

场景一:

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

假设我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 总可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数,那么我们就不能说是在运行镜像的时候添加 -i 参数。

之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,所以自然找不到命令。况且 -i 根本就不是一个命令。

那么如果我们希望加入 -i 参数,就只能完整的输入这个命令:

这显然不是一个好的解决方案,因此诞生了 ENTRYPOINT,使用ENTRYPOINT 就可以很好的解决命令被替换的问题。现在我们重新用 ENTRYPOINT 来实现这个镜像。

再次尝试执行 docker run myip -i

可以看到,这次是成功了,因为当 ENTRYPOINT 存在后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里的 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到预期的效果。

场景二

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

比如 mysql 数据库,可能需要一些数据库配置、初始化工作,这些工作需要在 mysql 服务器运行之前处理。

此外,可能希望避免使用 root 用户去启动服务,从而提供安全性,而在启动服务器之前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份,方便调试等。

这些准备工作适合容器 CMD 无关的,无论 CMD 是什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后范围 ENTRYPOINT 中去执行,从而这个脚本会将接收到的参数,作为命令,在脚本最后执行,比如 redis 中就是这么做的。

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINTdocker-entrypoint.sh 脚本。

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动容器,否则依旧使用 root 身份执行。例如:

LABEL

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)。

还可以用于一些标签来声明镜像的作者、文档地址等。

请务必使用双引号而不是单引号。特别是当您使用字符串插值(例如LABEL example="foo-$ENV_VAR")时,单引号将按原样获取字符串,而无需解压变量的值

USER

格式:

该指令设置当前阶段剩余部分指令的默认用户和组。指定的用户将用于RUNENTRYPOINTCMD指令的执行身份。

健康检查

HEALTHCHECK 指令告诉 Docker 如何检测容器否仍在运行。该指令有两种形式:

  • HEALTHCHECK [OPTIONS] CMD command:通过在容器内运行命令来检查容器健康状况。

  • HEALTHCHECK NONE:禁用从基础镜像继承的任何健康检查。

当容器指定了健康检查时,除了正常状态外,它还具有健康状态。此状态最初为starting。每当健康检查通过时,它就会变为healthy。在连续失败一定次数后,它会变为unhealthy

  • --interval=DURATION(默认:30s

  • --timeout=DURATION(默认:30s

  • --start-period=DURATION(默认:0s

  • --start-interval=DURATION(默认:5s

  • --retries=N(默认:3

健康检查将在容器启动后间隔几秒运行,然后在每次检查完成后间隔几秒再次运行。如果一次检查运行所用的时间超过超时秒数,则该检查被视为失败。

命令的退出状态表示容器的健康状态。可能的值包括:

  • 0:成功 - 容器健康且可供使用

  • 1:不健康 - 容器无法正常工作

  • 2:保留-不要使用此退出代码

例如,每隔5分钟检查一次web服务是否能够在三秒内提供网站主页的服务。

为了帮助调试失败的探测,命令在stdout或stderr上写入的任何输出文件都将存储在健康状态中,可以使用 docker inspect 进行查询。

SHELL

SHELL 指令可以指定RUN ENTRYPOINT CMD 指令的shell,Linux中默认为["/bin/sh", "-c"]

两个RUN运行同一条命令,第二个RUN运行的命令会打印出每条命令并当遇到错误时候退出。

ENTRYPOINT CMD 以 shell 格式指定时,SHELL 指令所指定的 shell 也会成为这两个指令的 shell

最后更新于

这有帮助吗?