强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

06 - CMD 与 ENTRYPOINT

06 - CMD 与 ENTRYPOINT:默认命令、入口点与信号处理

6.1 指令概述

CMDENTRYPOINT 都用于指定容器启动时运行的命令,但用途不同:

特性CMDENTRYPOINT
用途定义默认命令定义固定入口点
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

组合规则表

ENTRYPOINTCMD结果
未设置["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
nginxnginx -g daemon off;
httpdhttpd-foreground
postgrespostgres
redisredis-server
mysqlmysqld
pythonpython3
nodenode
mongomongod --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 扩展阅读


上一章05 - ENV 与 ARG 下一章07 - EXPOSE 与端口 — 端口声明、映射、多端口与健康检查。