基础

Docker从一个 Dockerfile 和一个 context 开始编译image

context 是一组文件,它们位于某个 PATH 或者 URL

context 是被循环加入的,其中的全部内容都会被发给dockerd

格式

Dockerfile 必须以 FROM 指令开头,它指定了base image,我们会基于这个base image开始build。

FROM 行前面也允许一些 ARG 指令,用于声明 FROM 行中的参数。

注释

注释只能单独存在一行。

解析命令

解析命令行也以 # 开始,用于定义一些影响整体编译的变量。在解析Dockerfile的时候一旦遇到任何注释,空行或者其他有效命令,那么Docker会把之后的所有以#开头的行认为是注释。因此,解析命令行一定要放在最开始。

环境变量

在一行上的环境变量的展开是同时生效的,例如:

ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

这里的defhello而不是bye,而ghibye

.dockerignore 文件

这个文件可以指定哪些文件不会作为context的一部分传给dockerd(例外是,如果 .dockerignoreDockerfile 也被屏蔽了的话,它们还是会被传给dockerd);同时,也作用于 ADDCOPY 指令。

Instruction

EXPOSE

这往往只是image作者给container执行者的一个信息,告诉她哪(几)个端口是会被使用的。而后续真正被公开到宿主机的方式是在启动container的时候使用 -p 或者 -P 指令。

ADD

拷贝文件到container中,如果是相对路径的话是从 WORKDIR 开始的。

COPY

拷贝文件到container中,和 ADD 的区别在于:

  1. ADD<src> 是某种压缩包,它会被解压缩
  2. ADD 接受URL作为 <src>
  3. COPY 支持 --from=<name|index> 选项,它允许以某个之前的 build stage 作为 <src> 文件的来源,而不是来源于默认的 context.

RUN CMD ENTRYPOINT

RUN

CMD

ENTRYPOINT

ENTRYPOINT && CMD

下面列出一个表格表示它们两个之间的关系,可能和关网的不太一样,但是这是我在本地(v18.01)实际试验的结果:

  没有ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”]
没有CMD Error! /bin/sh -c exec_entry p1_entry exec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec entry p1_entry exec_cmd p1_cmd
CMD “exec_cmd” “p1_cmd” exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

由上可见,最佳的实践是:使用 CMDexec form 给出默认参数;使用ENTRYPOINTexec form 给出执行指令和默认参数。

VOLUME

指定了某个目录是宿主机或者其他容器的目录的挂载点。

当这个命令执行之后,所有对该目录的改动命令都无效。

挂载(包括:volume, bind mount, tmpfs mount)的动作只能发生在运行时的参数,而不是编译时。如果运行时没有相关挂载参数,那么这个目录的内容是默认在编译时生成的内容。

ARG vs ENV

ENV 定义的变量一般用于被用于 ADD/COPY/WORKDIR/RUN 之类的命令,可以用于定制化编译时的行为,同时,它最后也会被export到image的环境变量集中。它的优先级高于ARG. 允许在运行docker的时候通过 docker run --env <key>=<value> 来修改。ENV 在编译全程有效。

ARG 一般用于RUN命令,也是用于定制化编译时的行为。它更多时候是为了允许用户在编译命令参数中传递不同的值而存在:docker build --build-arg <key>=<value>ARG 仅在当前编译stage (FROM) 有效。

SIGNAL

docker stop默认向容器的PID 1进程发送SIGTERM。如果没有使用exec form启动命令,那么你的进程是sh的子进程,也就是说sh是PID1进程,它负责接收信号;否则,dockerfile里指定的进程是信号的接收者。

这里有一个奇怪的现象是,不论那种情况,如果PID 1进程没有显示地注册信号处理函数,那么并不是使用默认的处理函数(例如SIGTERM默认是终止进程),而似乎是忽略该信号。因此,如果你使用exec form执行一个脚本,需要在脚本中是用trap明确地注册信号处理函数,否则docker stop的时候会超时触发强制停止,耗时比较长。

另一种做法是,使用tini作为PID1进程,这个程序在 Docker 1.13 以后已经内置在docker中了,使用docker run --init即可。它会将你指定的entrypoint作为tini的子进程,tini的生命周期取决于该子进程,并且退出码也是使用该子进程的退出码。同时,tini负责将收到的信号forward给这个子进程。这样,entrypoint中指定的程序就不需要显示地指定信号处理函数(除非默认行为不满足需求)。