Bootstrap

docker 中的entrypoint和cmd指令

一直知道这两者有区别,但是一直没有详细了解。最近在使用 docker 封装一个工具,来生成 license。正好可以作为一个案例来讲解两者的区别和用法

背景

计划使用 ruby 来生成 license 文件,因此需要在环境中:

  1. 安装 ruby
  2. 切换 gem 源(访问快),并安装项目依赖
  3. 编写 ruby 程序文件
  4. 执行 ruby 程序文件

计划基于 ubuntu:22.04 构建一个镜像,在镜像中安装好 ruby ,设置好 gem 源,安装项目依赖,源文件通过 volume 挂载到容器,最后通过 docker run 来执行源文件

构建镜像

FROM ubuntu:22.04
RUN apt update && apt install -y ruby && apt autoremove && apt-get autoclean && mkdir /workdir
RUN gem sources --add https://mirrors.tuna.tsinghua.edu.cn/rubygems/ --remove https://rubygems.org/
WORKDIR /workdir

上面的命令保存为 Dockerfile 文件,然后执行 docker build -t aa:aa . 来构建需要的镜像。

docker run 报错

构建一个项目目录,结构如下:

| context
| -- license.rb

我最初执行 docker run --rm -v /path/context:/workdir --entrypoint "ruby license.rb" aa:aa 但是报错

docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "ruby license.rb": executable file not found in $PATH: unknown.

重点是 unable to start container process: exec: "ruby license.rb": executable file not found in $PATH: unknown.

通过查阅文档,我们知道在 Dockerfile 中我们可以有两种方式指定 ENTRYPOINT,分别是:ENTRYPOINT ["executable", "param1", "param2"]ENTRYPOINT command param1 param2。我在这里想模拟的是第二种方式。

这两种方式的区别是:

  • ENTRYPOINT ["executable", "param1", "param2"] Docker 会直接调用命令,避免通过 Shell
  • ENTRYPOINT command param1 param2 Docker 会使用 /bin/sh -c 来执行命令

但这依然解释不了上面的报错。

通过询问 ChatGPT 得知,--entrypoint 的作用是覆盖镜像中指定的 ENTRYPOINT,从而定义容器启动时执行的主命令。但它仅接受单个命令(或可执行文件),而不是带参数的完整命令行。

问题解决

要想解决问题,并举一反三就要回到 ENTRYPOINTCMD 的作用上来。

ENTRYPOINTCMD 仅能使用一次,当出现多个 ENTRYPOINTCMD 时,仅最后一个 ENTRYPOINTCMD 生效。

Dockerfile 支持通过 shell 方式或者 exec 方式指定执行的命令及参数,但是两者有以下区别:

  1. 使用 shell 方式指定的命令及参数会以 /bin/sh -c 的方式执行。exec 方式指定的命令和参数会直接执行
  2. ENTRYPOINT 使用 shell 方式执行命令时,这个命令的进程不是 1 号进程,也就不能接收到 docker stop <container> 发送的 SIGTERM 信号
  3. ENTRYPOINT 使用 exec 方式执行的命令,进程 id 为 1,可以接收到 docker stop <container> 发送的 SIGTERM 信号
  4. 因为 exec 方式不是通过 shell 执行,而是直接执行命令,因此最好通过绝对路径的方式来运行命令

两者关系

  1. 当两者都没有指定时,容器自动退出。
  2. 当仅指定了 ENTRYPOINTCMD 时,指定了哪个,执行哪个

ENTRYPOINT 与 CMD 的结合

Shell 方式

执行逻辑:

  1. ENTRYPOINT 的内容通过 Shell 执行。
  2. CMD 的内容被拼接成字符串,并附加到 ENTRYPOINT 后面。
FROM ubuntu:latest
ENTRYPOINT echo
CMD ["hello", "world"]

---

FROM ubuntu:latest
ENTRYPOINT echo “aa”
CMD ["hello", "world"]

实际执行的命令:

/bin/sh -c "echo hello world"
---
/bin/sh -c "echo 'aa' hello world"

特点:

  1. CMD 被拼接为单个字符串,作为 Shell 的一部分执行。
  2. 容易受到 Shell 特性的影响,例如需要正确转义特殊字符。
Exec 方式

执行逻辑:

  1. ENTRYPOINT 和 CMD 被独立解析。
  2. 如果两者都存在,CMD 的参数直接传递给 ENTRYPOINT 的命令。
FROM ubuntu:latest
ENTRYPOINT ["echo"]
CMD ["hello", "world"]

实际执行的命令:

echo hello world

特点:

  1. 参数传递更直观,直接作为数组中的元素。
  2. 不需要考虑 Shell 特性,避免转义和环境差异问题。

CLI 和 Dockerfile 区别

当使用 CLI 时,--entrypoint 指令会覆盖 Dockerfile 中的 ENTRYPOINT 。但是此时 --entrypoint 仅支持可执行文件,不支持添加参数。

因此 docker run --rm -v /path/context:/workdir --entrypoint "ruby license.rb" aa:aa 可以按照以下任意形式修改

  1. docker run --rm -v /path/context:/workdir --entrypoint "ruby" aa:aa license.rb
  2. docker run --rm -v /path/context:/workdir --entrypoint "/bin/bash" aa:aa -c ruby license.rb
;