Dockerfile 中命令的两种书写方式的区别

最早的初衷是要研究一下运行 Docker 容器时如何向其传递参数,却冷不防掉入了另一个深渊,不得不关心起 Dockerfile 中命令(包括 RUN, CMD 和 ENTRYPOINT) 的两种不同写法上的区别。

所以呢,先要稍稍了解一下 Dockerfile 中 RUN, CMD, ENTRYPOINT 这三个指令

  1. RUN 执行命令并创建新的镜像层,常用于安装软件包。可以多个,为避免创建过多的镜像层,我们尽量把命令合在一起,用分号或 &&。它与容器运行期无关。
  2. CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够在启动容器时被覆盖。多个 CMD 只有最后一个是有效的
  3. 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="-Xms20" test
Error occurred during initialization of VM
Too small initial heap

出错信息告诉了我们 $JAVA_OPTS 被替换成了 "Xms20"。

假如我们换成 exec 格式的写法

ENTRYPOINT ["java", "$JAVA_OPTS", "-jar", "/app.jar"]

用上面同样的 docker run 命令

$ docker run -e JAVA_OPTS="-Xms20" 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 命令。

链接:

  1. RUN vs CMD vs ENTRYPOINT - 每天5分钟玩转 Docker 容器技术(17)
  2. Dockerfile reference #ENTRYPOINT
  3. 优雅的终止docker容器
  4. Shell form ENTRYPOINT example

类别: Docker. 标签: . 阅读(312). 订阅评论. TrackBack.

Leave a Reply

avatar