
在我们查阅 Dockerfile 的官方文档时, 有可能发现一些命令的功能重复(至少看起来干的事情差不多), 我已经在上文分析过 ADD 和 COPY 命令的区别(他们功能类似), 现在我们分析另外 2 个命令, 他们的功能也非常类似, 是 CMD 和 ENTRYPOINT.
尽管 ENTRYPOINT 和 CMD 都是在 docker image 里执行一条命令, 但是他们有一些微妙的区别. 在绝大多数情况下, 你只要在这 2 者之间选择一个调用就可以. 但他们有更高级的应用, CMD 和 ENTRYPOINT 组合起来使用, 完成更加丰富的功能.
ENTRYPOINT 还是 CMD?
从根本上说, ENTRYPOINT 和 CMD 都是让用户指定一个可执行程序, 这个可执行程序在 container 启动后自动启动. 实际上, 如果你想让自己制作的镜像自动运行程序(不需要在 docker run 后面添加命令行指定运行的命令), 你必须在 Dockerfile 里面, 使用 ENTRYPOINT 或者 CMD 命令
比如执行运行一个没有调用 ENTRYPOINT 或者 CMD 的 docker 镜像, 一定返回错误
plain text_snippet
Plain Text · 2 lines|
| $ docker run alpine |
| FATA[0000] Error response from daemon: No command specified |
大部分 Linu 发行版的基础镜像里面调用 CMD 命令, 指定容器启动后执行 /bin/sh 或 /bin/bash. 这样镜像启动默认进入交互式的 shell
译注: 3 个不同的 Linux 镜像(ubuntu, busybox, debian)都在 Dockerfile 的最后调用 CMD '/bin/bash'
启动 Linux 发行版的基础 container 后, 默认调用 shell 程序, 符合大多数人的习惯.
但是, 作为开发者, 你希望在 docker 镜像启动后, 自动运行其他程序. 所以, 你需要用 CMD 或者 ENTRYPOINT 命令显式地指定具体的命令.
覆盖(Overrides)
在写 Dockerfile 时, ENTRYPOINT 或者 CMD 命令会自动覆盖之前的 ENTRYPOINT 或者 CMD 命令.
在 docker 镜像运行时, 用户也可以在命令指定具体命令, 覆盖在 Dockerfile 里的命令.
比如, 我们写了一个这样的 Dockerfile:
如果根据这个 Dockerfile 构建一个新 image, 名字叫 demo
plain text_snippet
Plain Text · 8 lines|
| $ docker run -t demo |
| PING localhost (127.0.0.1) 56(84) bytes of data. |
| 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.051 ms |
| 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.038 ms |
| ^C |
| --- localhost ping statistics --- |
| 2 packets transmitted, 2 received, 0% packet loss, time 999ms |
| rtt min/avg/max/mdev = 0.026/0.032/0.039/0.008 ms |
可以看出ping命令在 docker 启动后自己执行, 但是我们可以在命令行启动 docker 镜像时, 执行其他命令行参数, 覆盖默认的 CMD
plain text_snippet
Plain Text · 2 lines|
| $ docker run demo hostname |
| 6c1573c0d4c0 |
docker 启动后, 并没有执行ping命令, 而是运行了hostname命令
和 CMD 类似, 默认的 ENTRYPOINT 也在 docker run 时, 也可以被覆盖. 在运行时, 用 --entrypoint 覆盖默认的 ENTRYPOINT
plain text_snippet
Plain Text · 2 lines|
| $ docker run --entrypoint hostname demo |
| 075a2fa95ab7 |
因为 CMD 命令很容易被 docker run 命令的方式覆盖, 所以, 如果你希望你的 docker 镜像的功能足够灵活, 建议在 Dockerfile 里调用 CMD 命令. 比如, 你可能有一个通用的 Ruby 镜像, 这个镜像启动时默认执行 irb (CMD irb).
如果你想利用这个 Ruby 镜像执行任何 Ruby 脚本, 只需要执行这句话:
plain text_snippet
Plain Text · 1 line|
| docker run ruby ruby -e 'puts "Hello" |
译注: ruby -e 'puts "Hello" 覆盖了 irb 命令
相反, ENTRYPOINT 的作用不同, 如果你希望你的 docker 镜像只执行一个具体程序, 不希望用户在执行 docker run 的时候随意覆盖默认程序. 建议用 ENTRYPOINT.
Docker 在很多情况下被用来打包一个程序. 想象你有一个用 python 脚本实现的程序, 你需要发布这个 python 程序. 如果用 docker 打包了这个 python 程序, 你的最终用户就不需要安装 python 解释器和 python 的库依赖. 你可以把所有依赖工具打包进 docker 镜像里, 然后用
ENTRYPOINT 指向你的 Python 脚本本身. 当然你也可以用 CMD 命令指向 Python 脚本. 但是通常用 ENTRYPOINT 可以表明你的 docker 镜像只是用来执行这个 python 脚本,也不希望最终用户用这个 docker 镜像做其他操作.
在后文会介绍如何组合使用 ENTRYPOINT 和 CMD. 他们各自独特作用会表现得更加明显.
Shell vs. Exec
ENTRYPOINT 和 CMD 指令支持 2 种不同的写法: shell 表示法和 exec 表示法. 下面的例子使用了 shell 表示法:
当使用 shell 表示法时, 命令行程序作为 sh 程序的子程序运行, docker 用 /bin/sh -c 的语法调用. 如果我们用 docker ps 命令查看运行的 docker, 就可以看出实际运行的是 /bin/sh -c 命令
plain text_snippet
Plain Text · 6 lines|
| $ docker run -d demo |
| 15bfcddb11b5cde0e230246f45ba6eeb1e6f56edb38a91626ab9c478408cb615 |
| $ docker ps -l |
| CONTAINER ID IMAGE COMMAND CREATED |
| 15bfcddb4312 demo:latest "/bin/sh -c 'ping localhost'" 2 seconds ago |
我们再次运行 demo 镜像, 可以看出来实际运行的命令是 /bin/sh -c 'ping localhost'.
虽然 shell 表示法看起来可以顺利工作, 但是它其实上有一些小问题存在. 如果我们用 docker ps 命令查看正在运行的命令, 会有下面的输出:
plain text_snippet
Plain Text · 5 lines|
| $ docker exec 15bfcddb ps -f |
| UID PID PPID C STIME TTY TIME CMD |
| root 1 0 0 20:14 ? 00:00:00 /bin/sh -c ping localhost |
| root 9 1 0 20:14 ? 00:00:00 ping localhost |
| root 49 0 0 20:15 ? 00:00:00 ps -f |
PID 为 1 的进程并不是在 Dockerfile 里面定义的 ping 命令, 而是 /bin/sh 命令. 如果从外部发送任何 POSIX 信号到 docker 容器, 由于 /bin/sh 命令不会转发消息给实际运行的 ping 命令, 则不能安全得关闭 docker 容器(参考更详细的文档:Gracefully Stopping Docker Containers).
译注: 在上面的 ping 的例子中, 如果用了 shell 形式的 CMD, 用户按 ctrl-c 也不能停止 ping 命令, 因为 ctrl-c 的信号没有被转发给 ping 命令
除了上面的问题, 如果你想 build 一个超级小的 docker 镜像, 这个镜像甚至连 shell 程序都可以没有. shell 的表示法没办法满足这个要求. 如果你的镜像里面没有 /bin/sh, docker 容器就不能运行.
A better option is to use the exec form of the ENTRYPOINT/CMD instructions which looks like this:
一个更好的选择是用 exec 表示法:
plain text_snippet
Plain Text · 1 line|
| CMD ["executable","param1","param2"] |
Let's change our Dockerfile from the example above to see this in action:
CMD 指令后面用了类似于 JSON 的语法表示要执行的命令. 这种用法告诉 docker 不需要调用 /bin/sh 执行命令.
我们修改一下 Dockerfile, 改用 exec 表示法:
plain text_snippet
Plain Text · 1 line|
| FROM ubuntu:trustyCMD ["/bin/ping","localhost"] |
重新 build 镜像, 用 docker ps 命令检查效果:
plain text_snippet
Plain Text · 9 lines|
| $ docker build -t demo . |
| [truncated] |
| $ docker run -d demo |
| 90cd472887807467d699b55efaf2ee5c4c79eb74ed7849fc4d2dbfea31dce441 |
| $ docker ps -l |
| CONTAINER ID IMAGE COMMAND CREATED |
| 90cd47288780 demo:latest "/bin/ping localhost" 4 seconds ago |
现在没有启动 /bin/sh 命令, 而是直接运行 /bin/ping 命令, ping 命令的 PID 是 1. 无论你用的是 ENTRYPOINT 还是 CMD 命令, 都强烈建议采用 exec 表示法,
ENTRYPOINT 和 CMD 组合使用
之前只讨论了用 ENTRYPOINT 或者 CMD 之一指定 image 的默认运行程序, 但是在某种情况下, 组合 ENTRYPOINT 和 CMD 能发挥更大的作用.
组合使用 ENTRYPOINT 和 CMD, ENTRYPOINT 指定默认的运行命令, CMD 指定默认的运行参数. 例子如下:
plain text_snippet
Plain Text · 1 line|
| FROM ubuntu:trustyENTRYPOINT ["/bin/ping","-c","3"]CMD ["localhost"] |
根据上面的 Dockerfile 构建镜像, 不带任何参数运行 docker run 命令
plain text_snippet
Plain Text · 16 lines|
| $ docker build -t ping . |
| [truncated] |
| $ docker run ping |
| PING localhost (127.0.0.1) 56(84) bytes of data. |
| 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.025 ms |
| 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.038 ms |
| 64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.051 ms |
| --- localhost ping statistics --- |
| 3 packets transmitted, 3 received, 0% packet loss, time 1999ms |
| rtt min/avg/max/mdev = 0.025/0.038/0.051/0.010 ms |
| $ docker ps -l |
| CONTAINER ID IMAGE COMMAND CREATED |
| 82df66a2a9f1 ping:latest "/bin/ping -c 3 localhost" 6 seconds ago |
上面执行的命令是 ENTRYPOINT 和 CMD 指令拼接而成. ENTRYPOINT 和 CMD 同时存在时, docker 把 CMD 的命令拼接到 ENTRYPOINT 命令之后, 拼接后的命令才是最终执行的命令. 但是由于上文说 docker run 命令行执行时, 可以覆盖 CMD 指令的值. 如果你希望这个 docker 镜像启动后不是 ping localhost, 而是 ping 其他服务器,, 可以这样执行 docker run:
plain text_snippet
Plain Text · 13 lines|
| $ docker run ping docker.io |
| PING docker.io (162.242.195.84) 56(84) bytes of data. |
| 64 bytes from 162.242.195.84: icmp_seq=1 ttl=61 time=76.7 ms |
| 64 bytes from 162.242.195.84: icmp_seq=2 ttl=61 time=81.5 ms |
| 64 bytes from 162.242.195.84: icmp_seq=3 ttl=61 time=77.8 ms |
| --- docker.io ping statistics --- |
| 3 packets transmitted, 3 received, 0% packet loss, time 2003ms |
| rtt min/avg/max/mdev = 76.722/78.695/81.533/2.057 ms |
| $ docker ps -l --no-trunc |
| CONTAINER ID IMAGE COMMAND CREATED |
| 0d739d5ea4e5 ping:latest "/bin/ping -c 3 docker.io" 51 seconds ago |
运行 docker 镜像, 感觉上和执行任何其他的程序没有区别 --- 你指定要执行的程序(ping) 和 指定 ping 命令需要的参数.
注意到参数 -c 3, 这个参数表示 ping 请求只发送 3 次, 这个参数包括在 ENTRYPOINT 里面, 相当于硬编码 docker 镜像中. 每次执行 docker 镜像都会带上这个参数, 并且也不能被 CMD 参数覆盖.
永远使用 Exec 表示法
组合使用 ENTRYPOINT 和 CMD 命令式, 确保你一定用的是 Exec 表示法. 如果用其中一个用的是 Shell 表示法, 或者一个是 Shell 表示法, 另一个是 Exec 表示法, 你永远得不到你预期的效果.
下表列出了如果把 Shell 表示法和 Exec 表示法混合, 最终得到的命令行, 可以看到如果有 Shell 表示法存在, 很难得到正确的效果:
plain text_snippet
Plain Text · 1 line|
| Dockerfile CommandENTRYPOINT /bin/ping -c 3CMD localhost /bin/sh -c '/bin/ping -c 3' /bin/sh -c localhostENTRYPOINT ["/bin/ping","-c","3"]CMD localhost /bin/ping -c 3 /bin/sh -c localhostENTRYPOINT /bin/ping -c 3CMD ["localhost"]" /bin/sh -c '/bin/ping -c 3' localhostENTRYPOINT ["/bin/ping","-c","3"]CMD ["localhost"] /bin/ping -c 3 localhost |
从上面看出, 只有 ENTRYPOINT 和 CMD 都用 Exec 表示法, 才能得到预期的效果
结论
如果你想让你的 docker image 做真正的工作, 一定会在 Dockerfile 里用到 ENTRYPOINT 或是 CMD. 但是请注意,这 2 个命令不是互斥的. 在很多情况下, 你可以组合 ENTRYPOINT 和 CMD 命令, 提升最终用户的体验.
✦ 卷终 · 柯航存卷 ✦