docker stop 默认向 pid 1 进程发 sigterm,但 jvm 不自动响应,需确保 java 是 pid 1 并注册 shutdown hook 才能优雅停机;spring boot 2.3+ 可通过 server.shutdown=graceful 简化实现。
当 docker 容器中运行 java 应用时,直接执行 docker stop 默认会向 pid 1 进程发送 sigterm 信号,但 jvm 默认**不会响应 sigterm 做优雅停机**——它只响应 sigint(如 ctrl+c)或收到 kill -15 后,**需显式注册 shutdown hook 才能捕获并执行清理逻辑**。关键在于:容器内 jvm 必须能真正收到并处理该信号,且进程必须是前台运行、作为 pid 1(否则信号可能被 init 进程拦截或忽略)。
确保 Java 进程是容器 PID 1 并前台运行
Docker 容器中,只有 PID 1 进程能直接接收 docker stop 发出的 SIGTERM。若用 sh -c "java ..." 或 bash -c 启动,Java 不是 PID 1,SIGTERM 会发给 shell,而 shell 默认不转发给子进程(JVM),导致 Java 完全收不到信号。
- ✅ 正确做法:使用
exec java -jar app.jar——exec替换当前 shell 进程,让 JVM 成为 PID 1 - ❌ 错误写法:
java -jar app.jar(shell 仍是 PID 1)、sh -c "java ..."、bash -c "..." - 在 Dockerfile 中推荐写法:
CMD ["java", "-jar", "/app.jar"](直接 exec 模式,最安全)
在 JVM 中注册 Shutdown Hook 处理 SIGTERM
JVM 本身不自动将 SIGTERM 映射为中断,但 Linux 下,当 JVM 作为 PID 1 收到 SIGTERM 时,只要没被屏蔽,JVM 会触发已注册的 Runtime.addShutdownHook()。这是实现优雅停机的核心机制。
- 在 main 方法启动后尽早注册钩子,例如:
log.info("Received shutdown signal, closing resources...");
server.shutdown(); // 如 Netty/Undertow/Tomcat 停止接收新请求
dataSource.close(); // 关闭数据库连接池
awaitTermination(); // 等待异步任务完成
}));
- 注意:Shutdown Hook 中避免长时间阻塞(默认最多等待约 10 秒,超时由 JVM 强制终止),应配合超时控制
- 不要在 hook 中调用
System.exit(),这会导致重复关闭或死锁
验证信号是否真正送达 JVM
常因镜像基础(如 Alpine + BusyBox sh)或 Java 版本差异导致信号行为异常,建议实测确认:
- 启动容器后,进入容器:
docker exec -it <container> ps aux</container>,确认java进程 PID 是 1 - 手动发送信号测试:
docker kill --signal=SIGTERM <container></container>,观察日志是否输出 Shutdown Hook 中的内容 - 若无响应,检查是否用了
glibc兼容问题(Alpine 需openjdk:17-jre-slim或加apk add --no-cache libc6-compat) - 可临时在 hook 中写文件或调用 curl 上报,排除日志异步丢失干扰
补充:Spring Boot 用户更简单的方式
Spring Boot 2.3+ 内置了优雅停机支持,无需手写 Shutdown Hook:
- 启用配置:
server.shutdown=graceful(application.yml) - 设置等待时间:
spring.lifecycle.timeout-per-shutdown-phase=30s - 底层仍依赖 JVM Shutdown Hook,但自动整合了 Web 容器(Tomcat/Netty)和 Spring Lifecycle Bean 的关闭顺序
- 仍需保证 Java 是 PID 1,否则配置无效











