一直知道这两者有区别,但是一直没有详细了解。最近在使用 docker 封装一个工具,来生成 license。正好可以作为一个案例来讲解两者的区别和用法
背景
计划使用 ruby 来生成 license 文件,因此需要在环境中:
- 安装 ruby
- 切换 gem 源(访问快),并安装项目依赖
- 编写 ruby 程序文件
- 执行 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 会直接调用命令,避免通过 ShellENTRYPOINT command param1 param2
Docker 会使用 /bin/sh -c 来执行命令
但这依然解释不了上面的报错。
通过询问 ChatGPT 得知,--entrypoint
的作用是覆盖镜像中指定的 ENTRYPOINT
,从而定义容器启动时执行的主命令。但它仅接受单个命令(或可执行文件),而不是带参数的完整命令行。
问题解决
要想解决问题,并举一反三就要回到 ENTRYPOINT
和 CMD
的作用上来。
ENTRYPOINT
和 CMD
仅能使用一次,当出现多个 ENTRYPOINT
或 CMD
时,仅最后一个 ENTRYPOINT
或 CMD
生效。
Dockerfile 支持通过 shell 方式或者 exec 方式指定执行的命令及参数,但是两者有以下区别:
- 使用 shell 方式指定的命令及参数会以
/bin/sh -c
的方式执行。exec 方式指定的命令和参数会直接执行 ENTRYPOINT
使用 shell 方式执行命令时,这个命令的进程不是 1 号进程,也就不能接收到docker stop <container>
发送的SIGTERM
信号ENTRYPOINT
使用 exec 方式执行的命令,进程 id 为 1,可以接收到docker stop <container>
发送的SIGTERM
信号- 因为 exec 方式不是通过 shell 执行,而是直接执行命令,因此最好通过绝对路径的方式来运行命令
两者关系
- 当两者都没有指定时,容器自动退出。
- 当仅指定了
ENTRYPOINT
或CMD
时,指定了哪个,执行哪个
ENTRYPOINT 与 CMD 的结合
Shell 方式
执行逻辑:
- ENTRYPOINT 的内容通过 Shell 执行。
- 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"
特点:
- CMD 被拼接为单个字符串,作为 Shell 的一部分执行。
- 容易受到 Shell 特性的影响,例如需要正确转义特殊字符。
Exec 方式
执行逻辑:
- ENTRYPOINT 和 CMD 被独立解析。
- 如果两者都存在,CMD 的参数直接传递给 ENTRYPOINT 的命令。
FROM ubuntu:latest
ENTRYPOINT ["echo"]
CMD ["hello", "world"]
实际执行的命令:
echo hello world
特点:
- 参数传递更直观,直接作为数组中的元素。
- 不需要考虑 Shell 特性,避免转义和环境差异问题。
CLI 和 Dockerfile 区别
当使用 CLI 时,--entrypoint
指令会覆盖 Dockerfile 中的 ENTRYPOINT
。但是此时 --entrypoint
仅支持可执行文件,不支持添加参数。
因此 docker run --rm -v /path/context:/workdir --entrypoint "ruby license.rb" aa:aa
可以按照以下任意形式修改
docker run --rm -v /path/context:/workdir --entrypoint "ruby" aa:aa license.rb
docker run --rm -v /path/context:/workdir --entrypoint "/bin/bash" aa:aa -c ruby license.rb