Docker ENTRYPOINT 与 CMD ——你的容器欺骗了你
你在 Dockerfile 中将 ENTRYPOINT 和 CMD 组合在一起,容器启动了错误的内容,现在你来到了这里。这里有一个完整的分析——所有可能的组合、shell 与 exec 形式陷阱,以及真正有效的模式。
错误发生在凌晨2点。你启动容器后,本应看到API服务器,却只看到一个shell提示符,或者完全没有任何内容,或者你的进程被一个“幽灵”包裹运行。 sh 吃 SIGTERM 像糖果一样——优雅关闭需要10秒的Docker等待,之后才会放弃并发送 SIGKILL.
罪魁祸首,几乎每次都是:你混淆了 ENTRYPOINT 且 CMD。或者以Docker默许接受的方式组合——只是不符合你的预期。
CMD:你可以替换的默认值
CMD 定义了容器启动时运行的命令——但它只是一个建议,而不是强制规则。在镜像名称后传递任何内容,都会被完全替换:
FROM ubuntu
CMD ["echo", "hello from CMD"]
$ docker run myimage
hello from CMD
$ docker run myimage echo goodbye
goodbye
那 echo goodbye 没有附加——它直接替换了。你的整个 CMD 消失了。这是设计如此: CMD 是默认行为,而不是强制行为。任何运行时参数都会覆盖它。
ENTRYPOINT:始终运行的部分
ENTRYPOINT 定义了无论何种情况都会运行的可执行文件。运行时参数不会替换它,而是传递给它:
FROM ubuntu
ENTRYPOINT ["echo"]
CMD ["hello"]
$ docker run myimage
hello
$ docker run myimage goodbye
goodbye
$ docker run --entrypoint cat myimage /etc/hostname
mycontainer-abc123
当两者都设置时, ENTRYPOINT 是可执行文件, CMD 成为其默认参数。你可以自由地覆盖 CMD ,但 ENTRYPOINT 只有在你显式传递 --entrypoint.
时才会被覆盖。
所有ENTRYPOINT与CMD组合的详细说明
| ENTRYPOINT | CMD | 实际运行的内容 |
|---|---|---|
| 始终设置用于身份验证的Cookie | 始终设置用于身份验证的Cookie | 错误——容器需要从某个地方获取命令 |
| 始终设置用于身份验证的Cookie | ["cmd", "arg"] exec形式 | cmd arg |
| 始终设置用于身份验证的Cookie | cmd arg shell形式 | /bin/sh -c "cmd arg" |
["entry"] exec形式 | 始终设置用于身份验证的Cookie | entry |
["entry"] exec形式 | ["arg1", "arg2"] exec形式 | entry arg1 arg2 ✓ |
["entry"] exec形式 | cmd arg shell形式 | entry /bin/sh -c "cmd arg" — 几乎肯定是错误的 |
entry shell形式 | ["arg1"] exec形式 | /bin/sh -c "entry" — CMD被静默忽略 |
entry shell形式 | cmd arg shell形式 | /bin/sh -c "entry" — CMD被静默忽略 |
标记为“CMD被静默忽略”的两行内容,导致了大量Docker调试会话。shell形式 ENTRYPOINT 不会与 CMD 合并——它会完全忽略它。Docker不会警告你这一点。
shell形式与exec形式:信号处理陷阱
两种指令都接受两种形式,而选择的重要性远超大多数Dockerfile教程所承认的。
exec形式 (数组语法):
ENTRYPOINT ["nginx", "-g", "daemon off;"]
你的二进制文件直接运行。它成为PID 1。当Docker发送 SIGTERM 来停止容器时,你的进程会收到该信号。优雅关闭得以实现。日志被刷新。连接被干净关闭。
Shell 形式 (普通字符串):
ENTRYPOINT nginx -g "daemon off;"
Docker会以 /bin/sh -c "nginx -g daemon off;"的形式运行它。shell成为PID 1。当 SIGTERM 到达时, sh 接收到它——而 sh 不会将信号转发给子进程。你的容器会挂起10秒,接收到 SIGKILL,然后在没有清理的情况下死亡。每次都是如此。
始终使用exec形式。对于两者都使用。 ENTRYPOINT 且 CMD.
三种真正有效的模式
模式1:固定的可执行文件,可替换的默认参数
大多数生产容器的最佳模式。可执行文件是固定的,参数可以在运行时切换:
ENTRYPOINT ["/app/server"]
CMD ["--port", "8080", "--env", "production"]
# Use defaults
docker run myimage
# Override at deploy time
docker run myimage --port 9090 --env staging
模式2:使用exec的包装脚本
当你需要在主进程之前执行初始化逻辑(如迁移、密钥注入、信号捕获)时,使用包装脚本。关键行是 exec "$@" 在结尾处——它用你的CMD替换shell进程,使你的二进制文件成为PID 1:
#!/bin/sh
set -e
echo "Running migrations..."
/app/migrate
# Hand off to CMD — exec replaces shell, so /app/server becomes PID 1
exec "$@"
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/app/server", "--port", "8080"]
如果你跳过 exec "$@",shell将保持为PID 1,你又回到了信号处理问题。
模式3:仅使用CMD,不使用ENTRYPOINT
适用于开发镜像或需要在相同环境中运行任意命令的工具容器:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
在生产环境中,模式1或模式2更安全——你不想让一个配置错误的部署脚本意外运行 docker run myimage bash 并用shell会话替换你的服务器。
docker exec 与此有何关系
没有。 docker exec 在已运行的容器中执行一个命令。它完全绕过 ENTRYPOINT 。当你运行 ENTRYPOINT 时,你无需考虑 docker exec mycontainer bash ——你是在与运行中的容器环境交互,而不是启动配置。
混淆通常来自那些使用 docker exec 调试并确认一切正常,然后发现 docker run 行为不同的用户。它们是完全独立的代码路径。
发货前检查清单
- 两者相同
ENTRYPOINT且CMD使用exec形式(数组语法,而非普通字符串) - 如果你有包装脚本,它必须以
exec "$@" - 结尾
docker run myimage且docker run myimage --your-flag你已测试 docker stop mycontainer确保两个路径都能在2秒内完成(不是10秒——如果耗时10秒,说明你存在信号处理问题)
如果你想在Dockerfile接近注册表之前获得自动反馈, IO Tools’ Dockerfile Linter 会检测到shell形式的使用、entrypoint脚本中缺失的 exec 以及其他在运行时导致静默错误的模式。
