← 全部笔记

Docker · 2 镜像与 Dockerfile

CMD 与 ENTRYPOINT 的区别

ENTRYPOINT 定入口,CMD 给默认命令或参数。先替换 CMD,再把参数接到 exec 形式的入口后面。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · 实验未实测 · 本页讨论 Linux 容器

保留入口

ENTRYPOINT ["echo", "entry"]

选择参数

CMD ["default"]

运行时给 reader → 整体换成 ["reader"]

最终参数数组

["echo", "entry", "reader"]

对应运行方式:docker run --rm demo reader(demo 是后面要构建的镜像)

图 1 两者都用 exec 形式时,运行参数替换 CMD,ENTRYPOINT 留在前面。

前置回顾:镜像、容器与仓库。示例假定镜像里有对应程序;基础镜像带来的默认配置也会参与组合。

先判断谁被替换

参数替换 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 表示执行后面的一段命令文本。

图 2 两种写法的差别在于,参数到达程序前是否经过 shell 解析。

镜像有 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。手机可在表格内部左右滚动。

每个单元格表示启动内容;shell 入口列按官方表展示有效命令,未列出不传给应用的尾部参数。
CMD ↓ / ENTRYPOINT →无 ENTRYPOINTshell:echo Eexec:["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 CMD
echo 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 是本实验选择的基础镜像标签,不代表最新版。构建日志取决于缓存与网络,本页不提供虚构日志。先预测下面五条命令各运行什么程序,再执行:

docker run --rm cmd-entrypoint-demo
docker run --rm cmd-entrypoint-demo reader
docker run --rm cmd-entrypoint-demo sh
docker run --rm --entrypoint /bin/sh cmd-entrypoint-demo -c 'echo debug'
docker run --rm --entrypoint /bin/echo cmd-entrypoint-demo

第 2 条会保留 default 吗?第 3 条会启动 sh 吗?第 5 条还会带着镜像的默认参数吗?写下理由,再对照预期结果。

预期文本(规则推导,未实测)
命令预期输出原因
第 1 条entry default入口 + 默认参数
第 2 条entry readerreader 替换 default
第 3 条entry shsh 只是 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 包装与 exec 替换时的信号路径 上半图是 shell 作为 PID 1 接收 SIGTERM,应用是子进程;下半图使用 exec 替换 shell,应用保留 PID 1,直接接收 SIGTERM。 A · shell 留在前面 ENTRYPOINT /app/server B · shell 用 exec 替换自己 exec /app/server "$@" /bin/sh · PID 1 启动子进程server · 子进程 SIGTERM → shell;未自动转发 shell · PID 1 exec 替换server · PID 1同一进程编号,程序已经替换 SIGTERM → server 自己处理
  1. shell 形式的入口先启动 /bin/sh -c,shell 占据 PID 1。
  2. shell 启动应用作为子进程。猜猜:docker stop 的 SIGTERM 会先发给谁?
  3. 先发给 shell;不能指望它自动转发给应用。
  4. 另一种启动脚本也先由 shell 运行,但准备使用 exec。
  5. exec 用应用替换 shell,保留 PID 1。猜猜:现在信号会发给谁?
  6. 信号直接送到应用;应用仍须实现合适的退出处理。
图 3 示意:多留一层 shell,就多一处信号转发责任;exec 替换能让应用直接接收信号。

图 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]

收起表格,再判断一次

要记住的

某镜像写着 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。

  1. Dockerfile reference:exec / shell 形式、CMD、ENTRYPOINT 与启动脚本、最终命令组合表及继承规则。
  2. Running containers · Default entrypoint:运行参数覆盖、--entrypoint 与清空 CMD。
  3. docker container stop:SIGTERM、SIGKILL 与可配置的停止等待。
  4. docker container run · Attach:Linux PID 1 的特殊信号行为。