[Docker] 如何向容器中传入参数
单纯为了解决这个问题,只需要在 Dockerfile
中将程序入口定义为:
1 | ENTRYPOINT exec command "$0" "$@" |
更全面一点,Dockerfile
中 RUN
CMD
ENTRYPOINT
有何不同,又分别适应什么样的场景?
请看下文
问题背景
在 微服务系列 中,遇到了一个问题:
当使用 docker run
运行容器并且传入参数时 ( command 为程序自定义的参数)
1 | $ docker-compose run user-cli command \ |
command
及之后的部分均无法传入容器中,遇到以下报错
1 | Error response from daemon: OCI runtime create failed: container_linux.go:346: starting container process caused "exec: \"command\": executable file not found in $PATH": unknown |
Docker 如何执行命令
Docker 容器由 Dockerfile
构建,容器执行的命令在 Dockerfile
中进行了规定,通常在 Dockerfile
中我们使用 RUN
CMD
ENTRYPOINT
执行命令,简单说明一下三者的用途:
RUN
命令执行指令并创建新的镜像层,通常用于安装软件包CMD
命令设置容器启动后默认执行的命令及参数,这个命令可以被docker run <image>
后面的命令行参数所替换掉ENTRYPOINT
同样也是配置容器启动时执行的命令,与CMD
不同的是,ENTRYPOINT
的指令一定会被执行
Shell 形式与 Exec 形式
RUN
CMD
ENTRYPOINT
(即下文 <INSTRUCTION>
) 都可以通过 Exec 与 Shell 两种形式运行命令:
Shell 格式:
<INSTRUCTION> <command> <param1> <param2>
,该格式即通过 shell 运行指令,在 Linux 上默认为/bin/sh -c
,Windows 上默认为cmd /S /C
;Exec 格式:
<INSTRUCTION> ["executable", "param1", "param2", ...]
,如RUN ["/bin/bash", "-c", "echo hello"]
与 Shell 格式不同,Exec 格式并不会调用一个 command shell 来执行指令 ( 比如
RUN [ "echo", "$HOME" ]
就不会有$HOME
变量显示出来,)。
事实上不管你使用的是 Shell 格式还是 Exec 格式,最终都会被转换为 Exec 格式,比如我们定义 CMD echo hello
,并检查镜像的信息 docker inspect <image>
,最终会发现,镜像中实际的 CMD
为:
1 | "Cmd": [ |
这里可以解释一个常见的误解,此处按下不表,且看后文。
RUN
RUN
的使用包括上文提到的两种格式:
RUN <command>
(Shell 格式)RUN ["executable", "param1", "param2"]
(Exec 格式)
RUN
指令会在原镜像之上建立一个新的镜像层并在其中中执行指令,新构建的镜像会在 Dockerfile
之后的步骤中被用到。
分层执行 RUN
指令并提交变动,生成新的镜像,这符合 Docker 的核心概念。就像源代码控制的核心概念一样,在Docker上,commit
是一个代价很低的操作,我们可以从映像历史记录的任何位置创建容器。
所以更普遍的,我们会使用 RUN
在当前镜像的顶部执行命令,创建一个新的镜像层,如:
1 | RUN apt-get update && apt-get install -y \ |
在这个镜像中,我们使用 apt-get update
来保证安装的包是最新的,并在其后指定安装了一些包,这里存在一个小坑,如果我们使用:
1 | RUN apt-get update |
可能会导致第一步 update
中使用的是很久以前缓存的一层镜像,这就导致软件包并没有如期望中更新。
当然,你也可以用 docker build --no-cache
来规避这个问题。
CMD
CMD
指令除了上文提到的两种格式之外,还有第三种格式:
CMD ["executable","param1","param2"]
(Exec 格式, this is the preferred form)CMD ["param1","param2"]
(为 ENTRYPOINT 提供默认参数)CMD command param1 param2
(Shell 格式)
需要注意的是,在 Dockerfile
中只能有一个 CMD
指令,多个 CMD
指令只有最后一个会生效。
CMD
指令的主要目的是为容器运行提供缺省指令,缺省指令可以包含一个执行指令,当然如果在 ENTRYPOINT
中有定义执行指令的话,CMD
也可以为其提供默认参数 (即)。
在 CMD
指令中,使用 Exec 格式是一个更好的选择,Exec 格式可读性更强,更容易理解,同时也能规避一些风险。
1 | FROM ubuntu |
当然,如果在 docker run -it <image> <command>
中附带了命令,<command>
就会代替 CMD
被执行,即 CMD
会被忽略,从之前提到的”多个 CMD 指令只有最后一个会生效“的规则中来看,这点也很好理解。
如果你想要每次运行容器的时候都执行相同的操作,请参考 ENTRYPOINT.
ENTRYPOINT
同样的,ENTRYPOINT
有两种形式:
Exec 形式,也是更为推荐的一种:
1
ENTRYPOINT ["executable", "param1", "param2"]
Shell 形式:
1
ENTRYPOINT command param1 param2
命令 docker run <image>
后附带的所有命令行参数都会作为新元素添加到 ENTRYPOINT [ "executable", "param1", "param2", ... ]
的后方,并且这些参数会完全取代 ‘CMD’ 中的指令。
你也可以使用
docker run --entrypoint
来重写ENTRYPOINT
的参数
为什么更推荐使用 Exec 形式而非看似更简单方便的 Shell 形式?
PID 1 进程
在 Shell 形式中,ENTRYPOINT
会作为 /bin/sh -c
的子命令来运行,PID 1
的进程会是 /bin/sh/
而非你所执行的程序,我们知道当使用 docker stop <CONTAINER>
的时候,容器会通过 SIGTERM
发送一个停止信号给 PID 1
的进程,并会等待10秒钟让程序自己退出,超时时才会使用 kill -9
情形停止。使用 Shell 形式会导致用户所运行的程序无法接受到信号量,会导致一些问题的出现。
使用 shell 内建命令 exec
ENTRYPOINT 中有一种声明格式可以使所运行的进程成为一个 PID 1
的超级进程,从而正常的接收信号量,即:
1 | ENTRYPOINT exec command param1 param2 ... |
或是(观察一下,其实二者是完全等价的):
1 | ENTRYPOINT [ "/bin/sh", "-c", "exec <PROCESS> <ARG1> <ARG2> ..." ] |
shell 的内建命令 exec
并不启动新的 shell ,而是用被执行的命令替换当前的 shell 进程,将老进程的环境清理掉,使 exec
后执行的进程成为一个 PID 1
的进程。
另外使用内建命令 exec
也可以使命令中如环境变量等参数被正确的解析,便于参数的传入。
注意:
exec
只会启动其后的第一个命令,如exec ls; top
只会执行ls
更多请参考官方文档。
传入参数的方法
此外,在使用内联环境变量的时候也需要注意,由于 Shell 格式总是由 /bin/sh -c
启动的,因此使用 Shell
格式可以比较方便地插入参数,如:
1 | ENTRYPOINT java $JAVA_OPTS -jar /app.jar |
直接在运行时使用如下命令即可将参数传入程序运行环境中:
1 | $ docker run -e JAVA_OPTS="-Xms20" test |
如果是 Exec
格式的 ENTRYPOINT 也希望能够解析变量,得这样写:
1 | ENTRYPOINT ["/bin/sh", "-c", "java $JAVA_OPTS -jar /app.jar"] |
注意 这样是行不通的,所有的参数都会作为 ENTRYPOINT ["/bin/sh", "-c", "java", ""$JAVA_OPTS", "-jar", "/app.jar"]
/bin/sh
的参数,而不是 java
的参数。
事先没法确定所有参数?
1 | ENTRYPOINT <command> <param1> "$0" "$@" |
更好的当然还是之前提到的
1 | ENTRYPOINT exec command "$0" "$@" |
$0
指执行程序
$@
指所有参数
ENTRYPOINT 与 CMD
CMD
与 ENTRYPOINT
都是用来在容器运行时执行指令的,关于它们之间的关系:
Dockerfile
中至少包含一个CMD
或ENTRYPOINT
- 当将容器作为可执行文件时,
ENTRYPOINT
必须被定义 CMD
通常为ENTRYPOINT
命令定义默认参数,或者用来执行一个 ad-hoc 指令
具体参见下方表格:
No ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENTRYPOINT [“exec_entry”, “p1_entry”] | |
---|---|---|---|
No CMD | error, not allowed | /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 [“p1_cmd”, “p2_cmd”] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd |
CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |
注意:
ENTRYPOINT
会将基础镜像中的CMD
重制为空值,需要在当前镜像层中重新定义CMD