Dockerfile 写作精讲 / 06 - CMD 与 ENTRYPOINT
06 - CMD 与 ENTRYPOINT:默认命令、入口点与信号处理
6.1 指令概述
CMD 和 ENTRYPOINT 都用于指定容器启动时运行的命令,但用途不同:
| 特性 | CMD | ENTRYPOINT |
|---|---|---|
| 用途 | 定义默认命令 | 定义固定入口点 |
被 docker run 参数覆盖 | ✅ | ❌(参数会追加) |
| 一个 Dockerfile 中数量 | 最后一条生效 | 最后一条生效 |
| 组合使用 | 作为 ENTRYPOINT 的默认参数 | 作为主程序 |
6.2 CMD 指令详解
三种形式
# 形式一:Exec 形式(推荐)
CMD ["executable", "param1", "param2"]
# 形式二:Shell 形式
CMD executable param1 param2
# 形式三:为 ENTRYPOINT 提供默认参数
CMD ["param1", "param2"]
Exec 形式 vs Shell 形式
# ✅ Exec 形式:直接执行进程,PID 为 1
CMD ["nginx", "-g", "daemon off;"]
# ❌ Shell 形式:通过 sh -c 执行,PID 不是 1
CMD nginx -g "daemon off;"
PID 1 的重要性:
Exec 形式:
PID 1 ──▶ nginx(接收信号)
Shell 形式:
PID 1 ──▶ /bin/sh -c "nginx -g daemon off;"
└── PID 2 ──▶ nginx(不接收信号)
关键:Docker 发送
SIGTERM时只发给 PID 1。如果 PID 1 是 shell 而非应用进程,应用将无法优雅退出,导致容器被强制SIGKILL。
CMD 被覆盖
FROM ubuntu:22.04
CMD ["echo", "Hello from CMD"]
# 默认行为
docker run myapp
# 输出: Hello from CMD
# 覆盖 CMD
docker run myapp echo "Overridden!"
# 输出: Overridden!
# 甚至可以覆盖为交互式 shell
docker run -it myapp /bin/bash
6.3 ENTRYPOINT 指令详解
两种形式
# Exec 形式(推荐)
ENTRYPOINT ["executable", "param1", "param2"]
# Shell 形式
ENTRYPOINT executable param1 param2
ENTRYPOINT 不会被覆盖
FROM ubuntu:22.04
ENTRYPOINT ["echo", "Hello"]
# docker run 的参数会追加到 ENTRYPOINT 之后
docker run myapp "World"
# 输出: Hello World
# 使用 --entrypoint 可以覆盖
docker run --entrypoint /bin/bash myapp -c "echo Overridden"
6.4 CMD 与 ENTRYPOINT 组合使用
这是最灵活的模式:ENTRYPOINT 定义主程序,CMD 提供默认参数。
FROM python:3.12-slim
WORKDIR /app
COPY . .
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080", "--host", "0.0.0.0"]
# 使用默认参数
docker run myapp
# 实际执行: python app.py --port 8080 --host 0.0.0.0
# 自定义参数(覆盖 CMD)
docker run myapp --port 9090 --debug
# 实际执行: python app.py --port 9090 --debug
组合规则表
| ENTRYPOINT | CMD | 结果 |
|---|---|---|
| 未设置 | ["cmd", "param"] | cmd param |
["ent", "param"] | 未设置 | ent param |
["ent", "param"] | ["cmd", "param"] | ent param cmd param |
| Shell 形式 | 任何 | CMD 被忽略 |
最佳实践:始终使用 exec 形式。如果需要 shell 特性,在 exec 形式中显式调用
sh -c。
6.5 SHELL 指令
SHELL 指令改变默认 shell,影响所有使用 shell 形式的指令。
# 默认 shell: ["/bin/sh", "-c"]
# Windows 容器默认: ["cmd", "/S", "/C"]
FROM python:3.12-windowsservercore
# 切换到 PowerShell
SHELL ["powershell", "-Command"]
RUN Get-ChildItem -Path C:\
RUN Write-Host "Hello PowerShell"
在 Linux 中使用 SHELL
FROM ubuntu:22.04
# 安装 bash 并设为默认 shell
RUN apt-get update && apt-get install -y bash
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
# 此后的 shell 形式指令都使用 bash -euo pipefail
# -e: 出错立即退出
# -u: 未定义变量报错
# -o pipefail: 管道中任意命令失败则整体失败
RUN echo "Using bash with strict mode" | grep "bash"
6.6 信号处理与 PID 1 问题
问题描述
# ❌ Shell 形式:应用不响应 SIGTERM
CMD node server.js
# PID 1 = sh,PID 2 = node
# docker stop 发送 SIGTERM 给 PID 1 (sh)
# sh 不会转发信号给 node
# 10 秒后 docker 强制 SIGKILL
# ✅ Exec 形式:应用直接接收 SIGTERM
CMD ["node", "server.js"]
# PID 1 = node
# docker stop 发送 SIGTERM 给 node
# node 可以优雅关闭
解决方案:tini
对于不处理信号的程序,使用 tini 作为 PID 1 的 init 进程:
FROM ubuntu:22.04
# 安装 tini
RUN apt-get update && apt-get install -y tini
# 使用 tini 作为 entrypoint
ENTRYPOINT ["tini", "--"]
# 应用作为 tini 的子进程
CMD ["python", "app.py"]
# Alpine 自带 tini
FROM alpine:3.19
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["myapp"]
tini 的作用
没有 tini:
PID 1 ──▶ node server.js
僵尸进程无法回收
有 tini:
PID 1 ──▶ tini
└── PID 2 ──▶ node server.js
tini 负责转发信号和回收僵尸进程
6.7 常见容器基础镜像的默认 CMD
| 镜像 | 默认 CMD |
|---|---|
nginx | nginx -g daemon off; |
httpd | httpd-foreground |
postgres | postgres |
redis | redis-server |
mysql | mysqld |
python | python3 |
node | node |
mongo | mongod --bind_ip_all |
6.8 实战模式
模式一:固定入口 + 默认参数
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["python", "-m"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# 默认运行 uvicorn
docker run myapp
# 替换为 gunicorn
docker run myapp gunicorn main:app -b 0.0.0.0:8000
# 替换为 pytest
docker run myapp pytest tests/
模式二:包装脚本
# docker-entrypoint.sh
#!/bin/bash
set -e
# 等待数据库就绪
if [ "$WAIT_FOR_DB" = "true" ]; then
echo "Waiting for database..."
while ! nc -z db 5432; do sleep 1; done
echo "Database is ready!"
fi
# 运行数据库迁移
if [ "$RUN_MIGRATIONS" = "true" ]; then
echo "Running migrations..."
python manage.py migrate
fi
# 执行 CMD 传入的命令
exec "$@"
FROM python:3.12-slim
WORKDIR /app
COPY . .
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
注意:entrypoint 脚本最后一行使用
exec "$@"将 CMD 的参数替换为当前进程,确保应用成为 PID 1 并能接收信号。
模式三:多命令容器(不推荐但有时需要)
# 使用 supervisord 管理多个进程
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY app.py /app/
COPY worker.py /app/
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
# supervisord.conf
[supervisord]
nodaemon=true
[program:app]
command=python /app/app.py
autorestart=true
[program:worker]
command=python /app/worker.py
autorestart=true
6.9 常见错误与排查
| 错误 | 原因 | 解决方案 |
|---|---|---|
| 容器启动后立即退出 | CMD 的进程退出了 | 检查前台运行(如 nginx 的 daemon off) |
docker stop 超时 | Shell 形式 PID 1 问题 | 使用 exec 形式或 tini |
| 僵尸进程累积 | PID 1 不回收子进程 | 使用 tini |
exec format error | 脚本缺少 shebang | 添加 #!/bin/bash |
| 参数未生效 | shell 形式忽略了 CMD | 使用 exec 形式 |
exec "$@" 无效 | 使用了 shell 形式 ENTRYPOINT | 改用 exec 形式 |
6.10 扩展阅读
- Docker CMD reference
- Docker ENTRYPOINT reference
- Docker SHELL reference
- tini — A tiny but valid init for containers
- PID 1 zombie reaping
上一章:05 - ENV 与 ARG 下一章:07 - EXPOSE 与端口 — 端口声明、映射、多端口与健康检查。