先判断谁被替换
参数替换 CMD
CMD 提供容器启动时的默认命令。ENTRYPOINT 定义入口程序。两者都在启动容器时生效;构建时执行命令用的是 RUN。[1]
docker run [选项] IMAGE [命令或参数…] 中,镜像名后只要给了命令或参数,就整体替换 CMD。没有入口时,这些内容自己组成命令;有 exec 形式的入口时,它们接在入口后面。
# 情形 A:只有 CMD
CMD ["echo", "default"]
# docker run --rm demo reader → 尝试执行 reader,不会执行 echo
# 情形 B:入口 + 默认参数
ENTRYPOINT ["echo", "entry"]
CMD ["default"]
# docker run --rm demo reader → 执行 echo entry reader
使用情形 B,运行 docker run --rm demo sh 会进入 shell 吗?先写出最终参数数组。
["echo", "entry", "sh"]。sh 替换默认的 default,成为 echo 的参数,所以不会启动 shell。要换入口,必须显式使用 --entrypoint。
要临时换成 shell,可运行 docker run --rm --entrypoint /bin/sh demo -c 'echo debug'。--entrypoint 放在镜像名前,只填程序名或路径;程序的参数放到镜像名后。它还会清空镜像原来的 CMD。[2]
再判断谁解释字符
shell 才会展开
exec 形式是 JSON 数组:每项都是一个独立参数,必须用双引号。shell 形式是一整段命令文本,由 shell(命令解释器)解析。Linux 默认使用 /bin/sh -c;本页假定没有用 SHELL 指令更改它。[1]
exec:直接传数组
CMD ["echo", "$HOME"]Docker → echo → 字面量 $HOME
没有自动启动 shell。$HOME、*、|、> 不会自动展开或成为管道、重定向。
shell:先解释文本
CMD echo "$HOME"Docker → /bin/sh -c → echo
shell 读取容器环境变量,再执行 echo。-c 表示执行后面的一段命令文本。
镜像有 ENV NAME=reader。CMD ["echo", "$NAME"] 与 CMD ["sh", "-c", "echo $NAME"],哪条会展开变量?为什么?
第二条会展开。两条语法上都是 exec 形式,但第二条主动执行了 shell。第一条把 $NAME 原样交给 echo;第二条让 sh 解释 echo $NAME。数组写法不保证运行的程序一定不是 shell。
你想默认运行 echo hello,也允许 docker run --rm demo sh 直接运行 sh。以下哪种配置合适?
exec 入口仍保留,sh 会成为 echo 的追加参数。
shell 入口不会被镜像名后的 sh 替换。
只有 CMD 时,运行参数能替换整个默认命令。选 B,并确保最终镜像配置中确实没有继承的 ENTRYPOINT。
把边界写进表里
组合看最终数组
下面把官方组合表改写成启动参数数组。同一个数组里的引号标明参数边界;shell 的整段命令文本是一项。这里暂不传运行参数。[1]
横向读:先选 ENTRYPOINT;纵向读:再选 CMD。手机可在表格内部左右滚动。
| CMD ↓ / ENTRYPOINT → | 无 ENTRYPOINT | shell:echo E | exec:["echo", "E"] |
|---|---|---|---|
| 无 CMD | 错误:没有启动命令 | ["/bin/sh", "-c", "echo E"] | ["echo", "E"] |
exec CMD["echo", "C"] | ["echo", "C"] | ["/bin/sh", "-c", "echo E"]CMD 不传给 echo | ["echo", "E", "echo", "C"]后一个 echo 也是参数 |
shell CMDecho C | ["/bin/sh", "-c", "echo C"] | ["/bin/sh", "-c", "echo E"]CMD 不传给 echo | ["echo", "E", "/bin/sh", "-c", "echo C"]shell 调用变成 echo 的参数 |
表中最容易看错的是右下角:Docker 没有先执行 CMD 再把输出交给入口。它把 shell 调用本身接到入口后面,echo 只会把这些参数当作文本。因此组合使用时,ENTRYPOINT 和 CMD 都优先写 exec 形式。
镜像配置为 ENTRYPOINT ["echo", "E"]、CMD ["C"]。运行 docker run --rm demo X Y。补全最终数组:
["echo", "E", "", ""]
答案依次为 X、Y。运行参数整体替换 ["C"],不会把 C 留下来;入口中的 E 仍保留。
深入:shell 入口为何“忽略参数”?
对于 ENTRYPOINT echo E,追加内容不会自动拼入字符串 echo E。在默认 sh 的 -c 规则下,后面的参数可以成为 shell 的 $0、$1 等;原命令没有引用它们,echo 就收不到。
这也解释了为什么 ENTRYPOINT exec echo E 能改善进程关系,却不会自动修好参数传递。需要接收运行参数时,用 exec 形式入口,或让入口脚本明确处理 "$@"(所有位置参数,保持各自边界)。
深入:继承与重复指令
一个 Dockerfile 有多条 CMD 或 ENTRYPOINT 时,各自只有最后一条生效,不会逐条执行。如果基础镜像定义了 CMD,而当前镜像重新设置 ENTRYPOINT,继承来的 CMD 会被清空;需要默认参数就重新写 CMD。
在你自己的 Docker 上验证
跑一次覆盖实验
编写环境没有 Docker。本节提供可复制命令和按官方规则推导的预期结果,不是实测终端记录;由维护者补实测输出。需要能运行 Linux 容器的 Docker Engine。
在宿主机终端新建实验目录。以下命令会写入当前目录的一个新文件夹,下载基础镜像并构建本地 demo 镜像。--rm 会在容器退出后删除该容器。
mkdir cmd-entrypoint-lab
cd cmd-entrypoint-lab
cat > Dockerfile <<'DOCKERFILE'
FROM alpine:3.22
ENTRYPOINT ["/bin/echo", "entry"]
CMD ["default"]
DOCKERFILE
docker build -t cmd-entrypoint-demo .
alpine:3.22 是本实验选择的基础镜像标签,不代表最新版。构建日志取决于缓存与网络,本页不提供虚构日志。先预测下面五条命令各运行什么程序,再执行:
第 2 条会保留 default 吗?第 3 条会启动 sh 吗?第 5 条还会带着镜像的默认参数吗?写下理由,再对照预期结果。
| 命令 | 预期输出 | 原因 |
|---|---|---|
| 第 1 条 | entry default | 入口 + 默认参数 |
| 第 2 条 | entry reader | reader 替换 default |
| 第 3 条 | entry sh | sh 只是 echo 的参数 |
| 第 4 条 | debug | 新入口 sh 执行命令文本 |
| 第 5 条 | 一个空行 | 新入口没有参数;原 CMD 已清空 |
如果实际结果不同,先核对镜像标签、Dockerfile 与命令中镜像名前后的参数位置。
再验证 shell 是否展开变量。-e 给容器设置环境变量;宿主机上的单引号保护 $NAME,让它留到容器内再解释。
docker run --rm -e NAME=reader --entrypoint /bin/echo cmd-entrypoint-demo '$NAME'
docker run --rm -e NAME=reader --entrypoint /bin/sh cmd-entrypoint-demo -c 'echo "$NAME"'
预期结果(推导、未实测):第一条显示字面量 $NAME,第二条显示变量值 reader。这里演示的是直接执行程序与显式调用 shell 的差异。
清理本地实验镜像:docker image rm cmd-entrypoint-demo。实验目录与基础镜像仍保留,可按需自行删除。
命令背后还有进程
信号先到 PID 1
PID 是进程编号。Linux 容器里的主进程通常是 PID 1。默认执行 docker stop 容器名 时,Docker 先向主进程发 SIGTERM(请求终止);超时未退出,再发 SIGKILL(强制终止)。停止信号与等待时间可以配置。[3]
- shell 形式的入口先启动
/bin/sh -c,shell 占据 PID 1。 - shell 启动应用作为子进程。猜猜:docker stop 的 SIGTERM 会先发给谁?
- 先发给 shell;不能指望它自动转发给应用。
- 另一种启动脚本也先由 shell 运行,但准备使用
exec。 exec用应用替换 shell,保留 PID 1。猜猜:现在信号会发给谁?- 信号直接送到应用;应用仍须实现合适的退出处理。
图 A 是 shell 保留为父进程的情形;某些 shell 会优化简单命令,实际进程树可能不同。可靠写法应明确表达意图:直接用 ENTRYPOINT ["/app/server"],或在启动脚本的最后使用 exec。[1]
#!/bin/sh
set -e
# 在这里做初始化;然后把控制权交给 CMD 指定的程序
exec "$@"
# 对应 Dockerfile 片段(/entrypoint.sh 需可执行):
# COPY entrypoint.sh /entrypoint.sh
# ENTRYPOINT ["/entrypoint.sh"]
# CMD ["/app/server", "--foreground"]
这里的 shell 内建命令 exec 会替换当前进程,与“exec 形式”的 JSON 语法是两个概念。"$@" 把收到的全部参数逐项传给程序;set -e 让脚本在通常的命令失败时退出。
脚本写成 /app/server "$@",没有 exec。为什么即使 Dockerfile 写的是 ENTRYPOINT ["/entrypoint.sh"],仍需检查停止行为?
exec 形式只决定如何启动入口。带 #!/bin/sh 的脚本仍由 shell 执行;若它留下来做父进程,PID 1 就是 shell。应用能否收到停止信号取决于脚本的转发或替换行为。
- 区分 JSON 数组形式与脚本内部的进程关系。
- 指出信号先发给 PID 1。
- 能提出用 exec 替换或显式转发信号。
深入:当了 PID 1 就能优雅退出吗?
不能保证。PID 1 在 Linux 中有特殊的默认信号行为,应用还应安装相应的信号处理逻辑。优雅退出指应用收到请求后完成清理,再自行退出。exec 解决的是信号接收路径,不能替应用实现清理。多子进程、回收退出子进程等问题留待后续进程管理章节。[4]
收起表格,再判断一次
要记住的
- 允许替换整个默认命令:使用 CMD;固定工具入口:使用 exec ENTRYPOINT + exec CMD。
- 镜像名后的参数整体替换 CMD;换入口用
--entrypoint,并重新给所需参数。 - exec 形式不自动解释 shell 语法;需要展开时明确调用 shell。
- 检查谁是 PID 1;启动脚本用
exec交出控制权,应用负责处理退出。
某镜像写着 ENTRYPOINT /app/server、CMD ["--port", "8080"]。你运行 docker run --rm demo --port 9090。最准确的判断是什么?
这条 ENTRYPOINT 是 shell 形式。参数没有被拼进原命令文本,也不能据此保证 server 是 PID 1。
运行参数已经替换 CMD;而且原 CMD 也不会自动传给这个 shell 入口里的 server。
改成 ENTRYPOINT ["/app/server"],保留 exec CMD 后,最终数组是 ["/app/server", "--port", "9090"]。若 server 是直接启动的应用,它会成为 PID 1;仍需它正确处理 SIGTERM。
来源
核对日期:2026-11-03。命令规则按 Docker 官方文档;图示为机制示意,浏览器内练习不会执行 Docker。
- Dockerfile reference:exec / shell 形式、CMD、ENTRYPOINT 与启动脚本、最终命令组合表及继承规则。
- Running containers · Default entrypoint:运行参数覆盖、--entrypoint 与清空 CMD。
- docker container stop:SIGTERM、SIGKILL 与可配置的停止等待。
- docker container run · Attach:Linux PID 1 的特殊信号行为。