Docker中的ENTRYPOINT与CMD

记住两点,就可以完全理解这两个关键字相关的所有用法与现象。

  1. ENTRYPOINT的默认值相当于是/bin/sh。(只是『相当于』。)
  2. 容器的执行入口为ENTRYPOINT CMD

相关概念

每个Dockerfile写到最后,都需要指定一个需要执行的命令,及其参数。 一个Docker容器,需要有且仅有一个前台进程在运行。 一旦这个进程结束了,那么容器本身的运行也就终结了。 而Dockerfile最后指定的这个命令,就是Docker容器的前台进程。

指定的方法,一般有两种,ENTRYPOINTCMD。 二者表面上看不出区别,实际上却有巨大差异,也有内在联系。

每个Docker容器,都必须要有一个可执行(executable)文件或命令、及相关参数,作为执行入口(entrypoint)。 所以,ENTRYPOINTCMD,二者在Dockerfile与docker run里,至少指定一个。

在执行docker run args... <image> cmd...时,镜像名称<image>后的所有内容,都被看做CMD。 它会覆盖镜像中写好的CMD(如果有的话)。

如果使用docker run --entrypoint /bin/bash <image> cmd,可以复写ENTRYPOINT为/bin/bash

使用区别

使用ENTRYPOINT和CMD的方式,大概分两种。 一是Dockerfile,二是docker run。 还有docker-compose.yml,其实和docker run差不多。

Dockerfile

No ENTRYPOINT ENTRYPOINT entry arg0 ENTRYPOINT [“entry”, “arg0”]
No CMD error, not allowed /bin/sh -c entry arg0 entry arg0
CMD [“cmd”, “arg1”] cmd arg1 /bin/sh -c entry arg0 entry arg0 cmd arg1
CMD cmd arg1 /bin/sh -c cmd arg1 /bin/sh -c entry arg0 entry arg0 /bin/sh -c cmd arg1

上表源于官方文档中的Understand how CMD and ENTRYPOINT interact,有所简化。 (原文应该是个中国人写的,竟然还用中文的引号!)

  1. ENTRYPOINT和CMD至少要有一个。
  2. 使用ENTRYPOINT entry arg0形式,与CMD将没有任何配合。因此,除非特定需求,否则不推荐这种使用方式。
  3. 右下角entry arg0 /bin/sh -c cmd arg1这种形式,几乎没有什么使用场景,反而是常见错误,应该尽量避免。

本质上,其实可以理解为ENTRYPOINT是真正的Docker可执行入口,而CMD则是可选参数。 之所以在很多情况下直接写CMD也能生效,是因为ENTRYPOINT就相当于是指定Shell,而CMD则是指定Shell中执行的命令。 注意,只是『相当于』。

docker run与docker-compose.yml

docker run <image> cmd arg1时,总是相当于CMD的["cmd", "arg1"]形式。 此外,docker-compose.yml的情况与docker run类似。

docker run中使用--entrypoint参数时,限制非常大。 仅仅只能指定一个命令、或可执行文件,不能指定一个参数列表。 例如:

$ docker run --entrypoint ls alpine /etc/passwd
/etc/passwd
$ docker run --entrypoint ls alpine -l /etc/passwd
-rw-r--r--    1 root     root          1224 Apr 24  2017 /etc/passwd

这时,如果希望把-l参数放到--entrypoint中,则无法做到。 (一些早期版本似乎是可以的。)

$ docker run --entrypoint 'ls -l' alpine /etc/passwd
container_linux.go:262: starting container process caused "exec: \"ls -l\": executable file not found in $PATH"
docker: Error response from daemon: oci runtime error: container_linux.go:262: starting container process caused "exec: \"ls -l\": executable file not found in $PATH".
ERRO[0001] error waiting for container: context canceled

--entrypoint中,无法指定一个列表。 不过,docker-compose.yml文件中,倒是可以。

    image: alpine
    entrypoint:
      - ls
      - -l
    command: /etc/passwd

注意PID

原则上,一个Docker容器里应该只有一个进程,其PID为1。 Docker外部的操作,比如docker stop,就是向这个进程发送信号。 如果那个唯一的前台进程PID不为1,那么就会收不到信号,只能在超时(默认约10秒)后被kill。

在Dockerfile中使用ENTRYPOINT entry arg0这种形式时,entry的位置总是应该使用exec,后面再接其它内容。 比如,ENTRYPOINT exec top,这可以确保top命令是PID为1的进程。 否则,ENTRYPOINT top的形式,PID为1的进程就是/bin/sh -c top,而top则被挤到了另外一个进程。

例如,对一个如下的Dockerfile:

FROM alpine
ENTRYPOINT top

执行以下的build与run操作:

docker build -t top .
docker run --rm -it --name test top

在另一个Shell中,可以查看到以下结果:

$ docker exec test ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/sh -c top
    7 root       0:00 top
    8 root       0:00 ps
$ time docker stop test

real	0m10.774s
user	0m0.008s
sys	0m0.004s

如果换成ENTRYPOINT exec top,或者ENTRYPOINT ["top"],就可以避免这个问题。

Docker版本

本文中描述的部分内容,在早期的Docker版本中并不正确。 撰写本文时,测试用的Docker版本信息如下。

$ docker version
Client:
 Version:      17.10.0-ce
 API version:  1.33
 Go version:   go1.8.3
 Git commit:   f4ffd25
 Built:        Tue Oct 17 19:02:43 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.10.0-ce
 API version:  1.33 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   f4ffd25
 Built:        Tue Oct 17 19:01:22 2017
 OS/Arch:      linux/amd64
 Experimental: false

相关笔记