最早的初衷是要研究一下运行 Docker 容器时如何向其传递参数,却冷不防掉入了另一个深渊,不得不关心起 Dockerfile 中命令(包括 RUN, CMD 和 ENTRYPOINT) 的两种不同写法上的区别。
所以呢,先要稍稍了解一下 Dockerfile 中 RUN, CMD, ENTRYPOINT 这三个指令
- RUN 执行命令并创建新的镜像层,常用于安装软件包。可以多个,为避免创建过多的镜像层,我们尽量把命令合在一起,用分号或 &&。它与容器运行期无关。
- CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够在启动容器时被覆盖。多个 CMD 只有最后一个是有效的
- ENTRYPOINT 配置容器启动时运行的命令。多个 ENTRYPOINT 也是只有最后一个有效
关于以上三个命令的区别,这儿有篇文章讲得很清楚 RUN vs CMD vs ENTRYPOINT - 每天5分钟玩转 Docker 容器技术(17),此处也照搬了些文字。
RUN, CMD 和 ENTRYPOINT 都支持两种写法,即 exec 和 shell 格式,见 Dockerfile reference #ENTRYPOINT 对这两种方式的解释。RUN 只影响如何构建镜像,所以镜像中不保留 RUN 命令。CMD 和 ENTRYPOINT 都可以在运行容器时执行命令,这里不讲述它们间的区别,而要说的是它们所支持的 exec 和 shell 两种格式的写法。此篇以 ENTRYPOINT 为例说明两种格式的区别,CMD 类似。
exec 格式
ENTRYPOINT ["executable", "param1", "param2"]
必须清楚了解命令 "executable" 的每一个参数,一个萝卜一个坑,不能随便乱拆与合并。例如执行 jar 文件的命令
java -Xmx256M -jar /app.jar
写成 exec 格式就是
ENTRYPOINT ["java", "-Xmx256M", "-jar", "/app.jar"]
而不能写成
ENTRYPOINT ["java", "-Xmx256M", "-jar /app.jar"]
否则 docker run
运行它时出错
Unrecognized option: -jar /app.jar
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
"-jar" 和 "/app.jar" 分别是两个参数。
exec 格式是一种数组形式
,该格式的 ENTRYPOINT 能接收 CMD 或 dock run <image>
后的参数作为附加参数,相当于是往这个数组中附加元素。例如 Dockerfile 中写成
ENTRYPOINT ["echo", "Hello"]
假设置构建出的镜像名(repository) 是 test(以下都以 test 作为镜像名称), 那么执行下面 docker 命令
$ docker run test World and China
输出是
Hello World and China
使用 exec 格式的 ENTRYPOINT 与 CMD 同在时还能接收 CMD 送过来的参数,如 Dockerfile
ENTRYPOINT ["echo", "Hello"]
CMD ["World"]
执行 docker run 命令
$ docker run test
输出
Hello World
另外,如果执行如下 docker 命令
docker run test China Haha
输出就是
Hello China Haha
原因是 CMD 在运行容器时由 docker run <image>
后的命令覆盖的了,所以 World
不见了。
注:exec 格式的 ENTRYPOINT 或 CMD 就是它们实际在 docker 镜像中的样子,可用 docker inspect <image>
查看。
shell 格式
ENTRYPOINT command param1 param2
官方也虽然定义了这么一种格式,其实它的确没什么特别之处。格式上其实就是平时怎么写完整命令 ENTRYPOINT 后就怎么写,没有中括号让你划分一个个参数,这是便利之处。同样的例子
ENTRYPOINT java -Xmx256M -jar /app.jar
为什么说 shell 格式没什么特别之处呢?因为只要对构建出的镜像用 docker inspect <image>
看下就知道怎么一回事
$ docker inspect test
观察到该 test 镜像的 ENTRYPOINT 实际上是
"Entrypoint": [
"/bin/sh",
"-c",
"java -Xmx256M -jar /app.jar"
],
因此也就是所谓 ENTRYPOINT 后那个完整 shell 命令最终是作为 "/bin/sh" 的第二个参数。同样的 CMD 的 shell 格式
CMD echo hello
inspect 看到该镜像中实际的 CMD 是
"Cmd": [
"/bin/sh",
"-c",
"echo hello"
],
CMD 后完整命令也是作为 "/bin/sh" 的第二个参数。
有了这个 shell 到 exec 格式的映射关系之后,我们就不难理解为什么 shell 格式的 ENTRYPOINT 不能接收 CMD 或 docker run
传过来的参数。因为参数将作为 "/bin/sh" 的参数而非 shell 的参数,举例说明:
对于 shell 格式
ENTRYPOINT java -Xmx256M -jar /app.jar
从 CMD 或 docker run
而来的参数( 比如 Hello World) 最终将会组成下面完整的 ENTRYPOINT
ENTRYPOINT ["/bin/sh", "-c", "java -Xmx256M -jar /app.jar", "Hello", "World"]
"Hello", "World" 并不是 java 命令的参数,而 "/bin/sh" 也会忽略掉它们。
为何 shell 格式可用变量而 exec 格式不一定行
最后一个问题,当我们使用 shell 格式时,总是可以使用内联的环境变量。例如在启动 java 程序时希望通过 JAVA_OPTS 来控制 JVM 参数,所以 ENTRYPOINT 这么写
ENTRYPOINT java $JAVA_OPTS -jar /app.jar
启动该镜像时用 -e 指定过小的堆内存大小报错
$ docker run -e JAVA_OPTS="-Xms2G" test
Error occurred during initialization of VM
Too small initial heap
出错信息告诉了我们 $JAVA_OPTS 被替换成了 "-Xms2G"。
假如我们换成 exec 格式的写法
ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", "/app.jar"]
用上面同样的 docker run
命令
$ docker run -e JAVA_OPTS="-Xms2G" test
Error: Could not find or load main class $JAVA_OPTS
从出错消息可以看出 exec 格式中的 "$JAVA_OPTS" 根本无法通过环境变量进行替换,原因是变量替换操作实际是由 "/bin/sh" 能完成的,shell 格式总是由 "/bin/sh -c" 启动的。如果 exec 格式的 ENTRYPOINT 也希望能解析变量,就得依样写成
ENTRYPOINT ["/bin/sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
其他各自特点(缺点)
exec 格式要求一个坑一个参数,所以像上面见到的那样无法在中间动态插入参数,比如不能在中间某一个位置上写上 "-Xmx5G -Xms2G", 这分明是两个参数,只能在后面附加参数
shell 格式由于命令总是由 "/bin/sh -e" 启动的子进程,它不是 PID 1 超级进程,从而无法收到 Unix 的信号,自然不能收到从 docker stop <container>
发来的 SIGTERM
信号。
简述一下 docker stop <container>
工作原理,它向容器中的 PID 为 1 进程发送 SIGTERM 信号,并给予 10 秒钟(可用参数 --time) 清理,超时才 -9 强杀,这样可以比较优雅的关闭容器。"/bin/sh -e" 是一个 PID 1 进程,它收到了 SIGTERM 却不会转发给它的子命令,这样就造成了 "/bin/sh -e" 收到 SIGTERM 未作响应被强杀,同时把它的子进程毫无征兆的干掉了。像在 Java 中用 Runtime.addShutdownHook()
是捕获不到该信号的。
增强型 shell 格式
这里补充一种 ENTRYPOINT 的声明格式,它实质是 shell 格式,为而把它单独列出来关键就在于 shell 的 exec
命令。此 exec
非前面 exec 格式中的 exec, 而是一个结结实实的 shell 命令。
ENTRYPOINT exec command param1 param2 ...
比如:
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar
它仍然是 shell 格式,所以 inspect 镜像后看到的 ENTRYPOINT 是
ENTRYPOINT ["/bin/sh", "-c" "exec java $JAVA_OPTS -jar /app.jar"]
然而加了 exec
的绝妙之处在于:
shell 的内建命令 exec 将并不启动新的shell,而是用要被执行命令替换当前的 shell 进程,并且将老进程的环境清理掉,exec 后的命令不再是 shell 的子进程序,而且 exec 命令后的其它命令将不再执行。从执行效果上可以看到 exec 会把当前的 shell 关闭掉,直接启动它后面的命令。
虽然它与之后的命令(如上 exec java $JAVA_OPTS -jar /app.jar
)还是作为 "/bin/sh" 的第二个参数,但 exec
来了个金蝉脱壳,让这里的 java
进程得已作为一个 PID 1 的超级进程,进行使得这个 java 进程可以收到 SIGTERM 信号。或者理解 exec
为 "/bin/sh" 的子进程,但是借助于 exec
让它后面的进程启动在最顶端。
另外,由于通过 "/bin/sh" 的搭桥,命令中的变量(如 $JAVA_OPTS) 也会被正确解析,因此 ENTRYPOINT exec command param1 param2 ...
是被推荐的格式。
注意:exec 只会启动后面的第一个命令,exec ls; top
或 exec ls && top
只会执行 ls
命令。
2023-12-18, 再次对比 java ... 与 exec java ... 的区别,发现又没有区别了
Dockerfile ENTRYPOINT:
ENTRYPOINT java $JAVA_OPTS -jar /app.jar "$0" "$@"
1 2 3 4 |
sh-4.2# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 00:01 ? 00:00:00 /dev/init -- /bin/sh -c java $JAVA_OPTS -jar /app.jar "$0" "$@" root 7 1 5 00:01 ? 00:00:38 java -Xms2g -Xmx2g /app.jar /bin/sh |
Dockerfile ENTRYPOINT:
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar "$0" "$@"
1 2 3 4 |
sh-4.2# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 00:26 ? 00:00:00 /dev/init -- /bin/sh -c exec java $JAVA_OPTS -jar /app.jar "$0" "$@" root 7 1 99 00:26 ? 00:00:46 java -Xms2g -Xmx2g -jar /app.jar /bin/sh |
ENTRYPOINT ["java", "-Xms2g", "-Xmx2g", "-jar", "/app.jar", "$0", "$@"]
1 2 3 4 |
sh-4.2# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 00:36 ? 00:00:00 /dev/init -- java -Xms2g -Xmx2g -jar /app.jar $0 $@ root 7 1 31 00:36 ? 00:00:40 java -Xms2g -Xmx2g -jar /app.jar $0 $@ |
链接:
[…] 其实只有第一种形式,紧随镜像名后那个总是一个命令,其后才是参数。如果要向 docker 容器传递参数时,Dockerfile 该如何写,这就有必要稍稍了解一下 Dockerfile 中 CMD 和 ENTRYPOINT 这两个指令,并且它们有 exec 和 shell 两种格式的写法。详情请见上篇 Dockerfile 中命令的两种书写方式的区别。 […]
[…] Dockerfile 中命令的两种书写方式的区别,其中提到过 Dockerfile 中可选择用 ENTRYPOINT 或 CMD 来启动进程,并且在 […]
写的很好,又get了新技能,感谢
写得太好了,看完才恍然大悟,刚刚在打包 Spring Boot 项目到 Docker 镜像的时候就遇到了
Error: Could not find or load main class $JAVA_OPTS
非常感谢博主!
请问可以转载吗 :-P
欢迎转载。能否加个友情链接?
哈哈,当然可以,已经加上了,见 Links(这两天有点事情才看到,不好意思 :-)
已加友情链接,谢谢
写的很好,又get了新技能,感谢