Skip to content

CMD 和 ENTRYPOINT

是什么

Dockerfile 里两个看着很像、实际完全不同的指令:

指令 作用 是否可被 docker run 覆盖
CMD 容器默认要执行的命令 ✅ 可被 run <image> <command> 整体替换
ENTRYPOINT 容器入口,可执行文件 ⚠️ 只有 --entrypoint 能改

两者同时存在时,CMD 的内容会作为参数传给 ENTRYPOINT。

                Dockerfile                  docker run 时
                ──────────                  ────────────

没有 ENTRYPOINT:                            CMD 可被覆盖
  CMD ["nginx"]                              docker run img ls
                                             → 跑 ls,不跑 nginx

没有 CMD 但有 ENTRYPOINT:                    ENTRYPOINT 不动
  ENTRYPOINT ["nginx"]                       docker run img -g "daemon off;"
                                             → 跑 nginx -g "daemon off;"

两个都有:                                    覆盖 CMD 部分
  ENTRYPOINT ["nginx"]                       docker run img -t
  CMD ["-g", "daemon off;"]                  → 跑 nginx -t

两种写法:shell vs exec

# Shell 形式(字符串):Docker 自动在外面包 /bin/sh -c
CMD nginx -g "daemon off;"
ENTRYPOINT nginx -g "daemon off;"

# Exec 形式(JSON 数组):原样执行,不走 shell
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT ["nginx", "-g", "daemon off;"]
写法 是否走 /bin/sh -c 能否处理环境变量/通配符 信号处理 推荐
shell 形式 ✅ 包了一层 ✅ 变量展开、$HOME*.log ❌ shell 会吃掉 SIGTERM ❌ 不推荐
exec 形式 ❌ 直接 exec ❌ 不展开,需用 shell 技巧 ✅ 信号直达 PID 1 ✅ 强烈推荐
shell 形式的实际执行(容器里看到的进程树):
   PID 1: /bin/sh -c "nginx -g 'daemon off;'"
            └── PID N: nginx            ← 收不到 SIGTERM

exec 形式的实际执行:
   PID 1: nginx -g daemon off;         ← PID 1 直接是 nginx
   ↑ 这才能正常接收 docker stop 发来的 SIGTERM

PS: 生产镜像一律用 exec 形式。 shell 形式会让 PID 1 变成 /bin/sh,容器收到 docker stopSIGTERM 会被 shell 吃掉,nginx 不会优雅退出,最后只能等 10 秒后 SIGKILL 强杀,丢数据。

CMD 详解

三个有效形式

# 1. exec 形式(推荐)
CMD ["nginx", "-g", "daemon off;"]

# 2. shell 形式
CMD nginx -g "daemon off;"

# 3. 给 ENTRYPOINT 传默认参数(最常见)
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]

覆盖规则

# 完整替换 CMD
docker run <image> <新命令>     # CMD 整个被替换
docker run <image>              # 用 Dockerfile 里的 CMD

PS:

  • 同一个 Dockerfile 里只能有一个 CMD,多个只有最后一个生效
  • docker run --entrypoint 也能改 ENTRYPOINT

ENTRYPOINT 详解

# exec 形式(推荐)
ENTRYPOINT ["nginx", "-g", "daemon off;"]

# shell 形式(基本不推荐,等同 CMD shell 形式)
ENTRYPOINT nginx -g "daemon off;"
# 覆盖 ENTRYPOINT
docker run --entrypoint="" <image> <新命令>
# 或者
docker run --entrypoint=/bin/sh <image>

典型用法:脚本包装

COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["default-arg1", "default-arg2"]
# entrypoint.sh
#!/bin/bash
set -e
# 等数据库起来
until pg_isready -h db; do sleep 1; done
# 处理配置
envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
# 跑主程序
exec "$@"   # 把 CMD 传过来的参数交给真正的主进程

PS: 脚本里 exec "$@" 是关键——把脚本进程"替换"成主进程,让主进程变成 PID 1,信号才收得到。

二者关系:默认参数模式

这是最常见也最推荐的形式:

ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
docker run myapp
   → 实际执行: python app.py --port 8080

docker run myapp --port 9090
   → 实际执行: python app.py --port 9090   ← CMD 被覆盖

镜像对外暴露一个"可执行程序"的接口,CMD 是默认参数,用户传参就是"调用"。

完整覆盖 ENTRYPOINT

docker run --entrypoint=/bin/sh myapp -c "ps aux"
# 完全替换 ENTRYPOINT(注意要传完整可执行文件路径)

实战模式

1. 官方 nginx 镜像

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

/docker-entrypoint.sh 负责:生成配置 → 软链默认配置 → 检查权限 → exec CMD 传过来的 nginx。

2. 跑数据库

COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["postgres"]

3. 单可执行程序

COPY myapp /usr/local/bin/
RUN chmod +x /usr/local/bin/myapp
ENTRYPOINT ["myapp"]
CMD ["--help"]   # 默认行为
docker run myapp                     # myapp --help
docker run myapp --version           # myapp --version
docker run myapp serve --port 80     # myapp serve --port 80

4. 一行启动多个进程(不推荐)

CMD sh -c "nginx && node server.js"

永远不要这么写。容器 = 单进程,多进程用 K8s Pod 多容器 / supervisord。shell 形式还会带来前面说的信号问题。

信号处理:PID 1 问题

容器内 PID 1 的进程不响应默认信号——因为 PID 1 只能被 init 处理,不会被 init 默认转发:

docker stop 发 SIGTERM → PID 1 收不收?
   shell 形式 CMD  → PID 1 是 /bin/sh  → 不转发,nginx 收不到
   exec 形式 CMD   → PID 1 是 nginx    → 正常收,优雅退出
   exec 形式 ENTRYPOINT (脚本) → 脚本里没 exec "$@" → 脚本收 SIGTERM
                                → 加上 exec "$@"    → nginx 收

三个解决套路

# 1. exec 形式 + exec "$@" 转发(最干净)
ENTRYPOINT ["entrypoint.sh"]

# 2. 用 tini / dumb-init 当 PID 1
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["nginx", "-g", "daemon off;"]

# 3. CMD 直接 exec 链
CMD ["sh", "-c", "exec nginx -g 'daemon off;'"]
推荐顺序:1 > 2 > 3
1 最干净;2 万能;3 丑但能跑

PS: K8s 场景特别注意:K8s terminationGracePeriodSeconds 默认 30s,给 docker stop 一次 10s 不够的话,改 K8s 配置。前提是你的容器能正确收 SIGTERM。

与 RUN 的区别

RUN apt-get install -y nginx        # 镜像**构建时**执行,结果写入镜像层
CMD ["nginx"]                       # 镜像**运行时**执行,不写入层
ENTRYPOINT ["nginx"]                # 镜像**运行时**执行
EXPOSE 80                           # 纯元数据,告知 Docker 这个容器用 80 端口

最佳实践

实践 为什么
一律 exec 形式 解决 PID 1 信号问题
ENTRYPOINT 用可执行文件,CMD 给默认参数 镜像对外像 CLI 工具
复杂启动逻辑用 ENTRYPOINT 脚本 + exec "$@" 比 shell 形式干净
脚本里用 set -e 出错立即退出
数据库等需要初始化用 ENTRYPOINT 脚本 比 CMD 单行塞不进去
调试时临时换 shell docker run --entrypoint=/bin/sh -it <image>
别用 CMD service nginx start 启动服务不要用 init.d 那套

常见坑

用了 shell 形式,docker stop 要等 10s

time docker stop myapp
# 10.0s 之后才退出
# 错的
CMD service nginx start
CMD nginx -g "daemon off;"   # 等价于 /bin/sh -c "nginx ..."

# 对的
CMD ["nginx", "-g", "daemon off;"]

JSON 数组里多个单词不引号会报 build 错

# 错:会被当成 shell 形式
CMD [nginx, -g, "daemon off;"]

# 对:JSON 字符串必须引号
CMD ["nginx", "-g", "daemon off;"]

exec 形式不展开变量

# 错:$PORT 是字面字符串,不会被 shell 展开
CMD ["node", "server.js", "--port", "$PORT"]

# 对 1:用 shell 形式(接受 PID 1 副作用)
CMD node server.js --port $PORT

# 对 2:先用 envsubst 或脚本处理
ENTRYPOINT ["./entrypoint.sh"]

多个 CMD / ENTRYPOINT 只最后一个生效

CMD ["a"]
CMD ["b"]    # 只有 b 生效

跑 Postgres / Redis 没设 initdb 路径

# 镜像里 postgres 命令不接受 -D 参数
CMD ["postgres"]
# 启动会失败,要用 entrypoint 脚本检查数据目录

调试

# 临时换 shell 进容器,看真实启动命令
docker run --entrypoint=/bin/sh -it <image>

# 看镜像的 ENTRYPOINT 和 CMD
docker inspect <image> | jq '.[0].Config.Entrypoint, .[0].Config.Cmd'

# 启动时附带参数,覆盖 CMD
docker run <image> --help

# 完全替换入口
docker run --entrypoint=ls <image> -la /app

参考