[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