https://yeasy.gitbooks.io/docker_practice/image/dockerfile/entrypoint.html
https://aws.amazon.com/cn/blogs/china/demystifying-entrypoint-cmd-docker/
Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD
指令就是用于指定默认的容器主进程的启动命令的。
Dockerfile 是应包含一条 ENTRYPOINT
说明、一条 CMD
说明,还是两者?
一般原则
ENTRYPOINT
+ CMD
= 默认容器命令参数
需要注意的是,在 Dockerfile
中,ENTRYPOINT
和 CMD
始终将转换为阵列 — 即使您声明它们为字符串。(但为避免歧义,我始终建议将它们声明为阵列。)
假设我们声明一个用于启动 Web 服务器的 CMD
如下:
CMD /usr/bin/httpd -DFOREGROUND
Docker 会自动将 CMD
转换为阵列,如下所示:
["/bin/sh", "-c", "/usr/bin/httpd -DFOREGROUND"]
这同样适用于 ENTRYPOINT
参数。
因此,当我们声明 ENTRYPOINT
和 CMD
时,ENTRYPOINT
将是一个列表,这两个参数将组合成为一个默认的参数列表 — 即使我们声明 CMD
为字符串。
请看下面的示例。如果我们声明如下:
ENTRYPOINT ["/bin/chamber", "exec", "production", "--"]
CMD "/bin/service -d"
默认的参数列表将为 ["/bin/chamber", "exec", "production", "--", "/bin/sh", "-c", "/bin/service -d"]
。
注意:ENTRYPOINT
和 CMD
不能同时为字符串值。 它们可以都是阵列值,或者 ENTRYPOINT
为一个阵列值而 CMD
为一个字符串值;但如果 ENTRYPOINT
为一个字符串值,则 CMD
将被忽略。
这是将参数字符串转换为阵列后无法避免的不幸后果。这也是我始终建议尽可能指定阵列的原因之一。
CMD 仅为默认值
指定 Dockerfile
中的 CMD
仅会创建一个默认值:如果我们将无选项的参数提交到 docker run
,则它们将被 CMD
的值覆盖。
为方便演示,假设我们拥有如下的 Dockerfile
,并从它创建了一个叫做 myservice
的映像:
ENTRYPOINT ["/bin/chamber", "exec", "production", "--"]
CMD ["/bin/service", "-d"]
如果我们调用 docker run myservice
,则将创建包含下列参数的容器:
["/bin/chamber", "exec", "production", "--", "/bin/service", "-d"]
如果我们改为调用 docker run myservice /bin/debug
,则将创建包含下列参数的容器:
["/bin/chamber", "exec", "production", "--", "/bin/debug"]
请注意 CMD
将被完全替换 — 不能在其开头或结尾处添加任何字符。
ENTRYPOINT 也可覆盖
我可以轻松覆盖在 Dockerfile
中声明的 ENTRYPOINT
。为此我们将指定 docker run
的 --entrypoint
选项参数。
如前面一样,假设我们拥有如下的 Dockerfile
,并从它创建了一个叫做 myservice
的映像:
ENTRYPOINT ["/bin/chamber", "exec", "production", "--"]
CMD ["/bin/service", "-d"]
然后让我们通过运行如下命令来修改 ENTRYPOINT
:
docker run --entrypoint /bin/logwrap myservice #entrypoint image cmd
根据我们的一般原则,将会构建如下参数列表:
["/bin/logwrap", "/bin/service", "-d"]
同时覆盖 ENTRYPOINT 和 CMD
我们能否同时覆盖 ENTRYPOINT
和 CMD
? 当然可以:
docker run --entrypoint /bin/logwrap myservice /bin/service -e
对应的参数列表如下 — 到这时应该不会发生任何意外了:
["/bin/logwrap", "/bin/service", "-e"]
我们何时应该使用 ENTRYPOINT? CMD 呢?
假设我们为某个项目构建自己的 Dockerfile
。我们已经了解了 ENTRYPOINT
和 CMD
如何结合起来构建容器的默认参数列表的原理。但现在我们需要知道如何选择:何时建议使用 ENTRYPOINT
,何时又建议使用 CMD
?
您所做的选择基本上属于一种艺术,它严重依赖您的使用案例。但我的经验是,ENTRYPOINT
几乎适合我遇到的所有案例。可以考虑下列使用案例:
封套
一些映像包含所谓的“封套”,将原有的程序进行装饰或者进行其他准备,以便于在容器化环境中应用。例如,假设您的服务设计为从文件读取配置,而非从环境变量读取。则在这种情况下,您可以包含一个将利用环境变量生成配置文件的封套脚本,然后在最后调用 exec /path/to/app
以启动应用程序。
声明指向封套的 ENTRYPOINT
就是确保封套始终运行的一个极佳方法,而不论将何参数发送到 docker run
。
单用途映像
如果您的映像仅用于执行一个操作( 例如运行 Web 服务器),则使用 ENTRYPOINT
来指定服务器二进制代码和任何强制参数的路径。一个典型示例是 nginx
映像,它的唯一用途是运行 nginx Web 服务器。这使它本身拥有一个天然的命令行调用:docker run nginx
。然后您可以顺理成章地在命令行后添加程序命令,例如 docker run nginx -c /test.conf
,就好比您在运行无 Docker 的 nginx。
多模式映像
支持多种“模式”的映像在 docker run <image>
上使用第一个参数来指定映射到模式的谓语(例如 shell
、migrate
或 debug
),也十分常见。对于此类使用案例,我建议 ENTRYPOINT
的设置应指向一个将解释动作参数并根据其值执行相关操作的脚本,例如:
ENTRYPOINT ["/bin/parse_container_args"]
这些参数将在调用时通过 ARGV[1..n]
或 $1
、$2
等发送到入口点。
小结
Docker 拥有极其强大与灵活的映像构建功能,但在确定如何构建容器的默认运行时参数方面可能极具挑战。但愿本文可以帮助您更加清楚参数-汇编机制,以及如何在您的环境中发挥它们的最佳作用。
#docker run [OPTIONS] IMAGE [COMMAND] [ARG...] #是CMD,而不是entrypoint。除非显示指定--entrypoint
ENTRYPOINT
的目的和 CMD
一样,都是在指定容器启动程序及参数。ENTRYPOINT
在运行时也可以替代,不过比 CMD
要略显繁琐,需要通过 docker run
的参数 --entrypoint
来指定。
当指定了 ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将 CMD
的内容作为参数传给 ENTRYPOINT
指令,换句话说实际执行时,将变为:
<ENTRYPOINT> "<CMD>"
那么有了 CMD
后,为什么还要有 ENTRYPOINT
呢?这种 <ENTRYPOINT> "<CMD>"
有什么好处么?让我们来看几个场景。
场景一:让镜像变成像命令一样使用
假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD
来实现:
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://ip.cn" ]
假如我们使用 docker build -t myip .
来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:
$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通
嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD
中可以看到实质的命令是 curl
,那么如果我们希望显示 HTTP 头信息,就需要加上 -i
参数。那么我们可以直接加 -i
参数给 docker run myip
么?
$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".
我们可以看到可执行文件找不到的报错,executable file not found
。之前我们说过,跟在镜像名后面的是 command
,运行时会替换 CMD
的默认值。
因此这里的 -i
替换了原来的 CMD
,而不是添加在原来的 curl -s http://ip.cn
后面。而 -i
根本不是命令,所以自然找不到。
那么如果我们希望加入 -i
这参数,我们就必须重新完整的输入这个命令:
$ docker run myip curl -s http://ip.cn -i
这显然不是很好的解决方案,而使用 ENTRYPOINT
就可以解决这个问题。现在我们重新用 ENTRYPOINT
来实现这个镜像:
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]
这次我们再来尝试直接使用 docker run myip -i
:
$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通
$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive
当前 IP:61.148.226.66 来自:北京市 联通
可以看到,这次成功了。这是因为当存在 ENTRYPOINT
后,CMD
的内容将会作为参数传给 ENTRYPOINT
,而这里 -i
就是新的 CMD
,因此会作为参数传给 curl
,从而达到了我们预期的效果。
例子: #Dockerfile_entrypoint FROM centos:centos6 RUN yum install curl ENTRYPOINT [ "curl", "-s", "http://ip.cn" ] #生成镜像 docker build -t dockerfile:entrypoint -f Dockerfile_entrypoint . #测试 [root@10.20.170.131 test]# docker run dockerfile:entrypoint 当前 IP:183.61.167.131 来自:广东省东莞市 电信 [root@10.20.170.131 test]# docker run dockerfile:entrypoint -i HTTP/1.1 200 OK Date: Tue, 13 Mar 2018 10:09:00 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Set-Cookie: __cfduid=dae038b394c996d3c4d0c3a3a228511241520935740; expires=Wed, 13-Mar-19 10:09:00 GMT; path=/; domain=.ip.cn; HttpOnly Server: cloudflare CF-RAY: 3fadb3d8404b935a-SJC 当前 IP:183.61.167.131 来自:广东省东莞市 电信
场景二:应用运行前的准备工作
启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
比如 mysql
类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。
此外,可能希望避免使用 root
用户去启动服务,从而提高安全性,而在启动服务前还需要以 root
身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root
身份执行,方便调试等。
这些准备工作是和容器 CMD
无关的,无论 CMD
为什么,都需要事先进行一个预处理的工作。
这种情况下,可以写一个脚本,然后放入 ENTRYPOINT
中去执行,CMD的参数作为这个脚本的参数,在脚本最后执行。
#Dockerfile_entrypoint2 FROM centos:centos6 COPY ./test.sh /data/ ENTRYPOINT [ "/data/test.sh" ] CMD [ "default_param1" ] #test.sh #!/bin/sh echo $1 echo $@ #生成镜像 docker build -t dockerfile:entrypoint2 -f Dockerfile_entrypoint2 . #测试 [root@ test]# docker run dockerfile:entrypoint2 param1 param1
上面的test.sh只能进行简单的处理,并不能处理命令行的docker run附带的命令,例如
docker run dockerfile:entrypoint2 /bin/bash
docker run -it dockerfile:entrypoint2 /bin/bash #我的本意是打算用test.sh执行一些预处理, 然后进行执行/bin/bash进入容器的的bash,但上面的test.sh不行。
需要改成下面这样:
#test.sh
#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'default_param1']; then
#do some work
elif [ "$1" = 'other_param1']; then
#do some other work
else
exec "$@" #关键点
fi
这样的话,命令docker run -it dockerfile:entrypoint2 /bin/bash在执行完test.sh的工作后,会进入容器的bash
docker run -it dockerfile:entrypoint2 #不带参数,使用默认参数default_param, 执行#do some work
docker run -it dockerfile:entrypoint2 default_param #带参数,参数是默认参数,执行#do some work
docker run -it dockerfile:entrypoint2 other_param #带参数,参数是other_param,执行#do other some work
docker run -it dockerfile:entrypoint2 /bin/bash#带参数,参数是/bin/bash,进入容器的bash
是做额外处理,还是分类处理,自己在test.sh中控制
#test.sh #!/bin/sh ... # allow the container to be started with `--user` if [ "$1" = 'some_cmd']; then #do some work fi
exec "$@" #关键点,始终执行
如果命令行docker run 传递的命令是some_cmd, 做一些预处理,然后执行exec $@
test.sh里面进行的软件安装不会影响image的大小,只有在dockerfile里进行软件安装才会影响image的大小。