在使用 Docker 的时候,不知道你有没有想过当我们敲下 docker run 之后,Docker 是怎么处理的?在以前我是没有想过的,但是在后来我看过一本讲关于 Docker 实现的书之后有了一些认识,可以认为是 Docker 在后台跑了一个进程,然后这个进程在执行你的程序。那么问题又来了,既然是跑进程,那么跑的这个进程是什么?为什么我明明只运行敲了一个: docker run -d mysql,docker 就知道我要在后台运行 Mysql 了?

当然,这些肯定都不是魔法,都是有迹可循的,而这些痕迹都是需要从 Dockerfile 中寻找,但是,在看了一些 Dockerfile 之后,发现有不同的写法,当然,除了常见的 RUN 等语句之外,通常有两个东西都是可以看见的,分别是 EntrypointCMD,他们提供类似的功能,但却不是一样的,所以这里就来聊聊他们的区别以及常用的方式。

在 Dockerfile 中,Entrypoint 和 CMD 都会在 docker run 的时候被用到,具体的被用的方式是:Entrypoint CMD,也就是说如果你同时指定了 Entrypoint 和 CMD,那么 Docker 就会将 Entrypoint 的内容作为主体,然后后面附加 CMD 的内容,例如这么一段:

那么当你执行 docker run test 的时候,实际上 docker 运行的进程就会是:

  1. /bin/sh -c "cat /etc/passwd"

我们来看下实际运行的结果:

这里和预期的是一致的。事实上,如果你不主动指定 Dockerfile 中的 Entrypoint,docker 默认就会使用 /bin/sh -c,这里注意,docker 是会有 ENTRYPOINT 的默认值的,但是,docker 却没有 CMD 的默认值,也就是说如果你不主动填写 CMD,那么就没有 CMD 的值;

ENTRYPOINT 和 CMD 的差异之一:如果不主动指定,ENTRYPOINT 有默认值,为:/bin/sh -c,但是 CMD 却没有默认值;

如果我们在编写 Dockerfile 的时候,什么都不指定,那么除了这里说的会有一个默认的 /bin/sh -c 的 ENTRYPOINT 之外,是不是就不能运行其他的进程了?很明显,根据我们平时的使用,并不是这样的,那么我们平时的运行的类似于:docker run -it test ping https://liqiang.io 是怎么个运行方式呢?其实,在 docker 中,如果你执行了 docker 的执行参数,那么这些参数都会被用来替换掉 CMD 的参数,也就是说,这里就相当于:

  1. CMD ping https://liqiang.io

但是,docker 却没有提供方式用来替换 ENTRYPOINT,也就是一旦你在 Dockerfile 中写死了 ENTRYPOINT,那么它将每次都被 Docker 执行。这里我还是以前面的 Dockerfile 为例子进行尝试,看下是不是这样的:

这里很明显,我们的执行命令确实是被替换掉了,但是,因为这个 Docker 网络的问题,导致 ping 不通,但是至少我们确实知道了 CMD 是可以被替换的。

ENTRYPOINT 和 CMD 的差异之二:ENTRYPOINT 不会被运行时替换,也不能被运行时替换,但是 CMD 会。

当我们查看文档的时候,会发现 CMD 和 ENTRYPOINT 都执行两种使用方式,而且 CMD 还比 ENTRYPOINT 多一种纯参数模式,ENTRYPOINT 和 CMD 的共同模式分别为:

  1. CMD ["executable", "param1", "param2"]
  2. CMD command param1 param2

那么既然有两种方式,那么肯定是有所差异的,他们之间的差异又是什么呢?首先在执行方式层面,他们就不一样,第一种 exec 模式,hi 被 docker 使用 fork-exec 的方式执行,这是推荐的方式,这样做的好处有:xxx;而第二种,会被放在 /bin/sh 中执行,好处是支持用户交互。如果从系统层面来看,通过 exec 方式运行,那么将不会关注到 Dockerfile 中执行的环境变量等,而通过 shell 方式运行的却是可以使用到 ENV 设置的环境变量;还有一个差别就是通过 exec 模型运行,那么进程在 Docker 中的 PID 就是 1,可以收到 docker 外部触发的系统信号;而通过 shell 运行的进程则收不到系统进程。

所以这里就存在一定的问题,因为有的时候我们的应用不能简单直白得就通过一个 exec 模式直接运行在 Docker 中,可能还需要处理一些环境相关的设置或者错误处理和故障恢复之类的,但是又系统能够很好得接受和处理系统信号,为了兼顾 exec 和 shell 两种模式的优点,一个被广泛使用的最佳实践就是通过一个 entrypoint.sh 的脚本作为 ENTRYPOINT,然后再 entrypoint.sh 中运行期望中的程序以及做信号处理等事情,这样就兼顾到了程序和环境的友好。例如这一个 postgres 的 Dockerfile 就是采用这种方式,而且他的 entrypoint.sh 也不复杂,有兴趣可以看一下:postgres entrypoint.sh

Reference

  1. What is the difference between CMD and ENTRYPOINT in a Dockerfile?