多进程容器必须用 init 进程解决 PID 1 问题,否则信号丢失、僵尸进程堆积、强制 kill-9;推荐 tini,轻量、自动转发信号和回收僵尸;Dockerfile 中设为 ENTRYPOINT 即可。

直接让应用当 PID 1 是最简方式,但只适用于单进程且能自己处理信号的场景;多进程容器必须引入 init 进程,否则信号收不到、僵尸停不掉。
为什么多进程容器一定要解决 PID 1 问题
Linux 内核规定 PID 1 进程必须主动回收僵尸子进程,并显式处理信号(比如 SIGTERM),否则:
- docker stop 发出的 SIGTERM 被内核丢弃,应用无法优雅关闭
- 子进程退出后变成 Z 状态(ps aux 显示 defunct),长期运行导致资源泄漏
- 容器最终被强制 kill -9 终止,可能丢失数据或连接
推荐用 tini 实现轻量级多进程管理
tini 是 Docker 官方推荐的 init 工具,体积小、无依赖、自动转发信号、自动 reap 僵尸进程。适合绝大多数 Python/Node.js/Go 等多进程场景:
- 在 Dockerfile 中安装并声明为 ENTRYPOINT:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["python", "app.py"] - 若用 Alpine 镜像:
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"] - tini 启动后会 fork 子进程(你的应用),并将所有收到的信号原样转发给它;子进程退出时自动 wait,不会留僵尸
替代方案:dumb-init 或自建 wrapper 脚本
如果已有基础镜像不支持 tini,或者需要更细粒度控制,可选:
- dumb-init:功能与 tini 类似,支持会话创建和信号广播到整个进程组,适合 shell 启动多个后台服务的场景
- 自定义 entrypoint.sh:需显式 trap SIGTERM/SIGINT,转发给主进程,并用 wait -n 监控子进程退出,同时调用 waitpid 防僵尸;但易出错,不建议新手使用
- 避免用
sh -c "your-command & wait"这类写法——shell 默认不转发信号,也不自动 wait 所有子进程
验证是否生效的简单方法
启动容器后,进容器执行:
ps -eo pid,ppid,stat,comm | head -10
确认 PID 1 是 tini(不是 sh 或 python);再执行:
docker stop -t 5 your-container
观察应用日志是否输出 shutdown 信息,而不是直接被 kill -9 终止。也可用 docker inspect 查 State.StoppedReason 字段判断是否正常退出。











