Docker容器优雅停止需三者协同:设置stop-timeout(如45s)、确保SIGTERM抵达应用(用exec格式或tini)、应用代码响应信号执行清理(如Spring Boot配置graceful shutdown)。
要让 docker 容器在停止时真正适配长连接应用(比如正在处理大文件上传、慢查询、websocket 长连接或批量数据刷盘),关键不是“停得快”,而是“等得准”——给应用留出足够时间完成清理,又不无谓空耗部署节奏。核心在于三处协同配置:容器运行时超时设置、应用层信号响应逻辑、以及启动方式保障信号可达。
运行时显式设置 stop-timeout
Docker 默认只等 10 秒,对多数 Web 服务够用,但对数据库迁移、日志归档、事务回滚等场景明显不足。必须主动延长:
- 单容器启动时加 --stop-timeout=45(单位秒):
docker run --stop-timeout=45 -d --name myapp myapp-image - Docker Compose 中写入服务配置:
services:
myapp:
image: myapp-image
stop_grace_period: 45s - 手动停止时指定时间:
docker stop -t 45 myapp
建议值参考:HTTP 长连接服务设 30–60s;带本地磁盘刷盘的 Java 应用设 45–90s;Kubernetes 场景下该值应 ≤ terminationGracePeriodSeconds,否则 Pod 可能被提前强制终止。
确保 SIGTERM 能真正抵达你的应用
很多“等不到结束”问题其实卡在信号没传进去。常见断点:
-
Shell 封装陷阱:CMD java -jar app.jar 实际启动的是 shell(PID 1),它不转发 SIGTERM。应改用 exec 格式:
CMD ["java", "-jar", "app.jar"] -
多进程无 init:若应用 fork 子进程(如日志收集器、健康检查守护进程),需轻量 init 工具接管信号分发。推荐 Alpine 下加 tini:
FROM alpine:latest
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["java", "-jar", "app.jar"] - 显式声明期望信号(可选但推荐):在 Dockerfile 加一行 STOPSIGNAL SIGTERM,避免某些镜像默认用 SIGQUIT 等非标准信号。
应用代码必须响应并执行清理
超时再长,应用不接信号也是白等。各主流语言典型做法:
- Java(Spring Boot):启用优雅关机,配置 server.shutdown=graceful 和 spring.lifecycle.timeout-per-shutdown-phase=45s;用 @PreDestroy 或实现 SmartLifecycle 做连接池关闭、缓存 flush。
- Node.js:监听 process.on('SIGTERM', () => { server.close(() => process.exit(0)); }),并在 close 回调中确保所有 pending 请求已响应。
- Go:用 signal.Notify(c, syscall.SIGTERM) 捕获,调 srv.Shutdown(ctx) 并等待返回,不直接 os.Exit。
验证是否生效:停止后查 docker logs myapp | tail -20,应看到类似 "shutting down server..." 或 "received SIGTERM, starting graceful shutdown" 的日志。
生产环境验证与兜底建议
光设了 timeout 不代表就稳了。上线前务必实测:
- 执行 docker stop -t 45 myapp 后,立刻 docker ps -a 观察状态变化;正常应先显示 Exited (0),而非 Exited (137)(表示被 SIGKILL 强杀)
- 若多次出现 137,说明要么超时仍不够,要么应用根本没响应 SIGTERM —— 此时别硬调高 timeout,先查日志和代码逻辑
- 编排场景(如 Compose/K8s)中,配合就绪探针(readiness probe)在收到 SIGTERM 后立即失败,防止新流量进入,这才是完整优雅闭环
不复杂但容易忽略:stop-timeout 是个桥梁,一端连 Docker 引擎,另一端必须连到你写的那行 signal handler 里。两端都到位,长连接才能真·从容收尾。











