[Docker] 如何向容器中传入参数

单纯为了解决这个问题,只需要在 Dockerfile 中将程序入口定义为:

1
ENTRYPOINT exec command "$0" "$@"

更全面一点,DockerfileRUN CMD ENTRYPOINT 有何不同,又分别适应什么样的场景?

请看下文

问题背景

微服务系列 中,遇到了一个问题:

当使用 docker run 运行容器并且传入参数时 ( command 为程序自定义的参数)

1
2
3
$ docker-compose run user-cli command \
--name="test" \
--email="test@test.com

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
2
3
4
5
"Cmd": [
"/bin/sh",
"-c",
"echo hello"
],

这里可以解释一个常见的误解,此处按下不表,且看后文。


RUN

RUN 的使用包括上文提到的两种格式:

  • RUN <command> (Shell 格式)
  • RUN ["executable", "param1", "param2"] (Exec 格式)

RUN 指令会在原镜像之上建立一个新的镜像层并在其中中执行指令,新构建的镜像会在 Dockerfile 之后的步骤中被用到。

分层执行 RUN 指令并提交变动,生成新的镜像,这符合 Docker 的核心概念。就像源代码控制的核心概念一样,在Docker上,commit 是一个代价很低的操作,我们可以从映像历史记录的任何位置创建容器。

所以更普遍的,我们会使用 RUN 在当前镜像的顶部执行命令,创建一个新的镜像层,如:

1
2
3
4
5
6
RUN apt-get update && apt-get install -y \  
bzr \
cvs \
git \
mercurial \
subversion

在这个镜像中,我们使用 apt-get update 来保证安装的包是最新的,并在其后指定安装了一些包,这里存在一个小坑,如果我们使用:

1
2
3
4
5
6
7
RUN apt-get update
RUN apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion

可能会导致第一步 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
2
FROM ubuntu
CMD ["/usr/bin/wc","--help"]

当然,如果在 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
2
3
ENTRYPOINT <command> <param1> "$0" "$@"

ENTRYPOINT [ "/bin/sh", "-c", "<command> <param1> \"$0\" \"$@\"" ]

更好的当然还是之前提到的

1
ENTRYPOINT exec command "$0" "$@"

$0 指执行程序

$@ 指所有参数


ENTRYPOINT 与 CMD

CMDENTRYPOINT 都是用来在容器运行时执行指令的,关于它们之间的关系:

  • Dockerfile 中至少包含一个 CMDENTRYPOINT
  • 当将容器作为可执行文件时,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