我们在运行 docker 镜像时希望能用下面的命令向容器传递命令行参数
docker run <image-name> <command> arg1 arg2
docker run <image-name> arg1 arg2
其实只有第一种形式,紧随镜像名后那个总是一个命令,其后才是参数。如果要向 docker 容器传递参数时,Dockerfile 该如何写,这就有必要稍稍了解一下 Dockerfile 中 CMD 和 ENTRYPOINT 这两个指令,并且它们有 exec 和 shell 两种格式的写法。详情请见上篇 Dockerfile 中命令的两种书写方式的区别。
对于一个 docker 镜像,我们可以这么来理解 ENTRYPOINT 与 CMD 的关系
- 如果没有定义 ENTRYPOINT, CMD 将作为它的 ENTRYPOINT
- 定义了 ENTRYPOINT 的话,CMD 只为 ENTRYPOINT 提供参数
- CMD 可由
docker run <image>
后的命令覆盖,同时覆盖参数
对于 #1 和 #2 更精致的理解是容器运行的最终入口由 ENTRYPOINT 和实际的 CMD 拼接而成。ENTRYPOINT 和 CMD 需转换为实际镜像中的 exec 格式来拼接,合并后的第一个元素是命令,其余是它的参数。
举四个例子进行说明
一, 未定义 ENTRYPOINT, 定义了 CMD
#ENTRYPOINT []
CMD ["echo", "hello"]
实际入口是它们拼接后还是 CMD 本身,["echo", "hello"]
二, 定义了 ENTRYPOINT 和 CMD
ENTRYPOINT ["echo", "hello"]
CMD ["echo", "world"]
实际入口是它们拼接起来,形成 ["echo", "hello", "echo", "world"]
, 执行 docker run test
显示为 hello echo world
三, 定义了 ENTRYPOINT, CMD 由 docker run
提供
ENTRYPOINT ["echo", "hello"]
执行命令 docker run <image> rm -rf /
, 实际入口是由 ["echo", "hello"]
与 ["rm", "-rf", "/"]
拼接而成的 ["echo", "hello", "rm", "-rf", "/"]
, 输出为 hello rm -rf /
。看到 rm -rf /
也不用担心,用 ENTRYPOINT 就是让人放心
注:ENTRYPOINT 同样可以被覆盖,如 docker run --entrypoint ls test -l /
,将会执行 ls -l /
命令。
四, 如果 ENTRYPOINT 用 shell 格式定义的
ENTRYPOINT java -jar /app.jar
CMD ["hello", "world"]
通过 docker inspect
命令看到镜像中实际的 ENTRYPOINT 是
ENTRYPOINT ["/bin/sh", "-c", "java -jar /app.jar"]
所以与 CMD 连接起来的入口就是 ["/bin/sh", "-c", "java -jar /app.jar", "hello", "world"]
, "bin/sh" 直接忽略掉后面的 "hello" 与 "world",这就是为什么 shell 命令方式无法获取参数。
有了以上几点概念,以及四个实例作为感观认识后,想要怎么往容器传递参数应该很容易确定了。
未定义 ENTRYPOINT
没有定义 ENTRYPOINT 的镜像想怎么来就怎么来,docker run <image>
后面的输入你自己作主。
有定义 ENTRYPOINT
定义了 ENTRYPOINT 的镜像,则是 CMD 或 docker run <image>
后的输入作为 ENTRYPOINT 中命令的附加参数。再次提醒 shell 格式的 ENTRYPOINT 和 CMD 务必要转换为相应 exec 格式来理解。
shell 格式的 ENTRYPOINT
如果是复杂的 shell 命令不容易拆解出一个个参数,而希望用 shell 格式来定义 ENTRYPOINT 的话,也有办法。shell 格式的 ENTRYPOINT 是由 "/bin/sh -c" 启动的,而它是可以解析变量的。另一方面 CMD 或 docker run <image>
的输入第一个元素存成了 $0
,其他剩余元素存为 $@
, 所以 shell 格式的 ENTRYPOINT 可以这么写
ENTRYPOINT echo hello $0 $@
注:shell 中 $0
表示命令本身,$@
为所有参数
这样执行下面 docker 命令将可获得所有的参数输入
$ docker run test world and China
hello world and China
如果只是按常规 shell 脚本来对待,想当然的写成
ENTRYPOINT echo hello $@
效果将是
$ docker run test world and China
hello and China
第一个参数将被丢失,docker run <image>
后第一个输入通常是一个命令,所以是 $0
, 而 ENTRYPOINT 又希望它是一个普通参数,因此$0 $@
要同时写上。
直接用 docker inspect <container-id>
查看
最简单且准确的方式就是直接用 docker inspect <container-id>
查看实际启动的命令及参数,不用猜测,docker inspect
也不会撒谎。对于如下 Dockerfile 定义
ENTRYPOINT java -Xmx256M -jar /app.jar
CMD ["echo", "hello"]
运行容器后,docker inspect <container-id>
, 注意是容器 ID 而非镜像 ID。在显示的 inspect JSON 结果的最顶端我们可以看到
1 2 3 4 5 6 7 |
"Path": "/bin/sh", "Args": [ "-c", "java -Xmx256M -jar /app.jar", "echo", "hello" ], |
ENTRYPOINT 和 CMD 如何组成实际入口,从这里的 Path
和 Args
是一目了然,也不用诧异为何 CMD 中的命令部分也变成了 ENTRYPOINT 命令的参数。
环境变量方式
对于 shell 格式的 ENTRYPOINT, 或者显式由 "/bin/sh -c" 来启动的命令,可以通过环境变量传入参数
ENTRYPOINT java $JAVA_OPTS -jar app.jar $0 $@
#或显式的 ENTRYPOINT ["/bin/sh", "-c", "java $JAVA_OPTS -jar /app.jar $0 $@"]
启动容器时的命令用
docker run -e JAVA_OPTS="-Xmx5G -Xms2G" <image-name> aa bb
那么实际执行 java 的完整命令就是
java -Xmx5G -Xms2G -jar /app.jar aa bb
此例结合了环境变更与 $0 $@
的方式。
总结
最好再重复一遍容器参数传递及 Dockerfile 配置的要领
- 容器运行的最终入口由 ENTRYPOINT 和实际的 CMD 拼接而成。
- ENTRYPOINT 和 CMD 合并前需转换为 exec 格式(用 docker inspect <image> 查看),合并后(相当于数组) 第一个元素是命令,其他都为参数
- CMD 可在 Dockerfile 中配置,在启动容器时会被
docker run <image>
后的参数覆盖 - CMD 的 exec 格式中,第一个元素是 shell 的 $0, 其余元素是 shell 的 $@。当 ENTRYPOINT 中用 shell 格式或显式的 sh(bash等)就可以引用 $0, $@
- 环境变量的解析是通过 sh(bash 等) 来解析的,所以
ENTRYPOINT ["echo", "$name"]
中的$name
是不被解析的 - 最能说明问题的是
docker inspect <container-id>
看个究竟,Path
和Args
说明了一切
补充(2018-04-06):
为了准确获取输入参数,对于 $0
和 $@
需要用引号括起来,像下面那样
ENTRYPOINT java $JAVA_OPTS -jar app.jar "$0" "$@"
#或 ENTRYPOINT ["/bin/sh", "-c", "java $JAVA_OPTS -jar /app.jar \"$0\" \"$@\""]
#更时髦一点的 shell exec 方式
# ENTRYPOINT exec java $JAVA_OPTS /-jar app.jar "$0" "$@"
下面解析为何 $0
和 $@
要用引号括起来,比如下面的 ENTRYPOINT
ENTRYPOINT java -jar /app.jar $0 $@
并且 /app.jar 中的 java main 方法如下:
1 2 3 |
public static void main(String[] args) { System.out.println("Arguments: " + String.join(", ", args)); } |
看下面的执行
$ docker run test "aa bb" cc "dd ee"
Arguments: [aa, bb, cc, dd, ee]
原本希望的是输入的三个参数 aa bb
, cc
和 dd ee
, 可是在 java 里被认为是 5 个参数。通过 inspect 启动的容器
"Path": "/bin/sh",
"Args": [
"-c",
"java -jar /app.jar $0 $@",
"aa bb",
"cc",
"dd ee"
],
说明参数的传入是没有问题,问题出在 $0 $@ 处,对 java -jar /app.jar $0 $@
展开后的效果是
java -jar /app.jar aa bb cc dd ee
所以被打散后失去原有的组织关系,变为 5 个参数了.
而如果给 $0
, $@
加上引号后
ENTRYPOINT java $JAVA_OPTS -jar /app.jar "$0" "$@"
构建镜像,重新执行
docker run test "aa bb" cc "dd ee"
Arguments: [aa bb, cc, dd ee]
这次参数的接收也没问题,加引号的方式可以让 java 接收到的参数保持原本的输入格式,即相当于
java -jar /app.jar "aa bb" cc "dd ee"
当然如果你的参数中不含有空格的话加不加引号都不会有事。
本文链接 https://yanbin.blog/pass-arguments-to-docker-container/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
很详细了,,不像网上各种吵来吵去的
也参考了不少,上面的例子都是不停的在本地 docker build, docker rmi 测试出来结果。
网上搜资料跳到您这儿,看了您如此详尽的blog肃然起敬。