故障排查 · 2026 年 11 月 11 日

Docker 容器启动即退出:ENTRYPOINT、信号与 PID 1

本地 docker run 正常,部署后容器秒退;或者跑着跑着停了,日志最后一行是 Starting。从退出码、ENTRYPOINT 写法到 PID 1 的信号语义,一次讲清。

docker ps -a 里一行 Exited (0) 3 seconds ago,日志最后一句是 Starting nginx...。本地 docker run 明明能起来,一进 CI 或生产就秒退。

还有一种更迷惑的表现:容器跑了几个小时,突然停掉,退出码 143,日志里没有任何错误。这两种现象指向同一条链路——谁是 PID 1,以及它有没有正确处理信号。

现象:先读退出码#

退出码 含义 常见原因
0 主进程正常结束 命令后台化,前台 shell 返回
1 应用自身报错 配置缺失、连不上依赖、参数错误
126 命令不可执行 文件没有执行权限、挂载覆盖了二进制
127 命令不存在 PATH 不对、拼写错误、壳脚本缺解释器
137 被 SIGKILL(128+9) 内存超限被 OOM kill,或 stop 超时后被强杀
143 被 SIGTERM(128+15) 正常 stop;若伴随未优雅退出,说明信号没被处理
139 段错误(128+11) 原生扩展崩溃、架构不匹配

docker stop 的流程是:向容器内 PID 1 发 SIGTERM,等 --time(默认 10 秒),还没退出就 SIGKILL。所以 143 和 137 的差别,往往就是「优雅退出有没有生效」。

还有一种退出码 0 但服务没起来的情况:容器显示 Running,健康检查却一直失败,编排层反复重启。这不是启动问题,是就绪判断缺失。日志里可能只有一句启动成功,实际业务端口并没有监听。

重启策略会把问题藏起来:restart: always 让容器反复退出又拉起,docker ps 里一直显示 Up,只有 docker inspect -f '{{.RestartCount}}' 的数字在涨。排查前先看这个计数。

定位:四条命令分清楚#

bash
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Command}}'
docker inspect -f 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} err={{.State.Error}}' <容器>
docker inspect -f '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' <容器>
docker logs --tail 100 <容器>

OOMKilled=true 就把方向转到内存限制;err 非空通常是运行时层面的错误,比如挂载失败、权限问题;Config.Entrypoint 与 Config.Cmd 是数组还是字符串,直接暴露了 shell form 还是 exec form。

启动即退的,绕过 ENTRYPOINT 进容器手动跑一遍,能直接看到第一行报错:docker run --rm -it --entrypoint sh <镜像>。进不去说明镜像里没有 shell(比如 distroless),那就得靠日志和本地复现。

想看时间线,用 docker events --filter container=<容器名> 盯着,退出、重启、OOM 都会打点,比翻日志更直观。

要区分「启动即退」和「运行中退出」:前者几乎总是命令写错或依赖缺失;后者要看信号与 PID 1。判断方法是看日志有没有走到「服务已就绪」那一行。

日志停在 Starting 之后再无输出,说明主进程已经退到后台;日志完全为空但退出码是 0,说明 CMD 本身就是个前台跑完即结束的脚本,容器自然跟着退出。

根因:四种写法,一个信号问题#

命令后台化了。 CMD service nginx start 这类写法,启动脚本把服务 fork 到后台后自己退出,容器的主进程结束,容器随之停止。Docker 只关心 PID 1,不关心后台子进程。

shell form 让 sh 变成 PID 1。 CMD python app.py 实际执行的是 /bin/sh -c "python app.py"。多一层 shell,信号先到 sh,sh 未必转发给 python。应用收不到 SIGTERM,stop 只能等超时后强杀。

脚本末尾没有 exec。 常见于 entrypoint 脚本:先做迁移、改权限,最后 python app.py。此时 PID 1 仍是 bash,SIGTERM 打到 bash 上,bash 不转发,应用被 137 强杀。加一个 exec 就能让应用替换掉 shell,直接接管 PID 1。

PID 1 不回收僵尸进程。 内核不会给 PID 1 的孤儿进程自动找父进程回收。shell 脚本作为 PID 1 时,子进程退出后会留下僵尸,长跑容器里进程表会被慢慢占满。

再多说一句信号语义:Linux 内核对 PID 1 有特殊处理——没有注册处理函数的信号,默认动作会被忽略。所以在容器里,SIGTERM 发给 PID 1 时,如果 PID 1 没装处理器,它不会像普通进程那样直接死掉,而是被忽略,直到 SIGKILL。

下面是最小对照:

dockerfile
# 错误一:shell form + 后台启动,主进程立刻退出
CMD service nginx start

# 错误二:shell form,sh 是 PID 1,信号到不了应用
CMD python app.py

# 正确:exec form,应用直接成为 PID 1
CMD ["nginx", "-g", "daemon off;"]

entrypoint 脚本则统一这样收尾:

bash
#!/bin/sh
set -euo pipefail

# 初始化:等依赖、跑迁移、修权限
/app/wait-for-db.sh

# 关键一行:用 exec 让应用替换当前 shell,成为 PID 1
exec "$@"

exec "$@" 依赖 CMD 传入参数,所以 ENTRYPOINT 用 exec form 时,CMD 也要用数组形式,两者配合才能把参数原样传下去。

修复:前台运行,信号可达#

  1. 应用一律前台运行。nginx 加 daemon off;,gunicorn 不要加 -D,supervisor 类工具要么不用,要么只托管一个前台进程。
  2. CMD / ENTRYPOINT 用 exec form(JSON 数组),避免 sh 包一层。
  3. entrypoint 脚本最后一行必须是 exec "$@"。
  4. 需要优雅退出时显式声明 STOPSIGNAL,例如 nginx 用 SIGQUIT。
  5. 加 HEALTHCHECK,让编排层区分「进程还在」和「服务可用」。
dockerfile
FROM nginx:stable
COPY nginx.conf /etc/nginx/nginx.conf
STOPSIGNAL SIGQUIT
HEALTHCHECK --interval=10s --timeout=2s --retries=3 \
  CMD curl -fsS http://127.0.0.1/health || exit 1
CMD ["nginx", "-g", "daemon off;"]

不想改镜像的话,运行时加 --init 也能解决大半问题:Docker 会注入一个小型 init 作为 PID 1,由它转发信号并回收僵尸进程。代价是多一个进程,好处是脚本类入口不用逐个改造。

改完后验证一次信号链路:docker stop 应该让应用走完优雅退出,退出码是 143 而不是 137。如果容器里有长连接或正在处理的请求,观察日志里有没有「等待连接排空」这类记录。

如果 docker inspect 显示 OOMKilled=true,处理方向完全不同:先调内存限制,再排查泄漏,反复重启只会把问题变成周期性抖动。

防复发#

  • CI 里加一步冒烟:docker run -d 后等 5 秒,用 docker inspect 断言容器仍在运行。
  • 再断言 docker stop 后退出码为 143,说明 SIGTERM 被应用处理了,而不是超时强杀。
  • Dockerfile review 检查清单:CMD 是否 exec form、是否前台、是否有 exec "$@"。
  • 日志全部输出到 stdout/stderr,不写文件,否则容器退出后什么都看不到。
  • 编排层设置合理的 terminationGracePeriodSeconds,与应用自身的优雅退出时限对齐。
  • 用 ps -ef | grep defunct 抽查长跑容器,僵尸进程数量应该稳定在 0。

结论#

容器「启动即退」先看退出码:127 是命令找不到,0 是主进程结束得太早,1 是应用自己报错。运行中停掉则看 143 与 137 的差别。

九成的信号问题来自三件事:命令后台化、CMD 用了 shell form、entrypoint 脚本结尾忘了 exec。修法固定:前台运行、exec form、exec "$@"。

还需要理解一条内核语义:PID 1 没有默认信号动作。信号发了但没反应,不是 Docker 的问题,是容器里那个人没装处理器。

最后补一层验证——CI 里断言「起来后 5 秒还在跑」和「stop 后退出码 143」,比任何文档都可靠。

完

#Docker#排障#Linux
回到顶部 ↑