docker stop 通过向 pid 1 进程发送 sigterm 实现优雅停止,关键在于确保所有子进程同属一个进程组并能响应该信号;推荐使用 tini 等 init 工具或手动实现信号透传与进程组广播,避免 shell 后台作业无法接收信号的问题。
要让容器内多个进程在 docker stop 时同步退出,关键不是“转发信号”本身,而是确保所有需清理的进程都能被统一管理并响应 sigterm。docker 只向 pid 1 进程发信号,其他进程默认收不到——所以必须构建一个能协调子进程生命周期的主控机制。
让 PID 1 成为真正的进程管理者
很多镜像用 sh -c "cmd1 & cmd2 & wait" 启动,看似多进程,但 shell 默认不转发信号,且 wait 只等前台进程,后台作业可能被忽略。正确做法是使用专为容器设计的 init 类工具或手动实现信号透传:
- 推荐用
tini(Docker 官方推荐轻量 init):在 Dockerfile 中加ENTRYPOINT ["tini", "--", "your-start-script.sh"],它会自动将SIGTERM转发给整个进程组 - 若不用外部工具,ENTRYPOINT 必须是可执行文件(非 shell 中转),且该程序需显式调用
signal.Notify(Go)、Runtime.addShutdownHook(Java)或signal.signal(signal.SIGTERM, handler)(Python),并在 handler 中主动 kill 所有子进程 - 避免
CMD ["bash", "-c", "service1 & service2 & wait"]这类写法——bash 不会把SIGTERM传给后台子进程,wait也只阻塞在第一个前台进程上
统一进程组 + 信号广播
Linux 支持进程组信号广播,前提是所有业务进程属于同一 PGID,且由 PID 1 主动控制:
- 启动时用
set -o monitor(启用作业控制),再以command &启动各服务,最后用wait -n或循环wait -p监听任意子进程退出 - 在 SIGTERM 处理函数中,调用
kill -- -$$(注意双破折号和负号),向当前进程组所有成员发送终止信号;配合trap 'kill -- -$$' TERM即可实现一键广播 - 各子进程自身也应监听
SIGTERM并完成本地清理(如关闭监听端口、刷盘日志),不能只依赖父进程强杀
验证与调试技巧
别等上线才发现信号没生效,本地快速验证很关键:
- 运行容器后,进容器执行
ps -eo pid,ppid,pgid,sid,comm,确认所有业务进程 PGID 一致且等于 PID 1 - 手动测试:
docker kill --signal=SIGTERM <container></container>,观察各进程是否在几秒内陆续退出(可用ps轮查) - 检查退出码:
docker inspect <container> --format='{{.State.ExitCode}}'</container>,优雅退出应为 0 或 143(表示收到 SIGTERM);若为 137,说明超时被 SIGKILL 强杀,说明清理逻辑卡住或未响应 - 日志中加入信号捕获打印,例如 Python 中
print("Received SIGTERM, shutting down..."),确认 handler 确实触发
结合注册中心的下线协同
微服务场景下,仅停进程不够,还需通知注册中心下线:
- 在 SIGTERM 处理逻辑最开始,先调用服务下线 API(如 Nacos 的
/nacos/v1/ns/instance?ephemeral=false&serviceName=xxx&ip=xx&port=xx&clusterName=DEFAULTDELETE) - 下线成功后再开始关闭监听、等待长连接结束;可设置最大等待时间(如 15 秒),超时则强制退出,避免无限阻塞
- Kubernetes 中建议配合
preStop生命周期钩子:在容器收到 SIGTERM 前,先执行一次下线请求,再留给应用 10–30 秒做剩余清理











