Docker 显示容器是 Up,并不代表应用已经准备好接收流量;显示 unhealthy 也不等于 PID 1 已退出。运行状态描述主进程,健康状态来自镜像或 Compose 中定义的独立检查命令。把两者混为一谈,常见结果是误杀仍在提供部分能力的进程,或把尚未就绪的实例加入负载均衡。
适用范围与核心结论
本文适用于 Docker 或 Compose 服务显示 running/unhealthy、编排平台不分发流量或依赖服务启动失败。验证环境使用 Docker 24.0.7、Compose 2.40.0 与 Nginx 1.28.3 Alpine。
第一步必须分别证明:PID 1 是否运行、健康检查执行了什么、最近一次检查为何失败、应用端点是否仍响应。不要先删除健康检查或无限增加重试次数。
第一步:同时读取运行状态与健康状态
docker inspect --format '
status={{.State.Status}}
running={{.State.Running}}
exit={{.State.ExitCode}}
health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}
pid={{.State.Pid}}' app
running=true 说明主进程仍在;health=unhealthy 说明健康命令连续失败达到重试阈值。若没有 State.Health,容器没有配置 Docker HEALTHCHECK,不能把 Up 当作健康证明。
第二步:读取实际健康命令和失败输出
docker inspect --format '{{json .Config.Healthcheck}}' app
docker inspect --format '{{json .State.Health.Log}}' app
日志中的 ExitCode、Output、开始和结束时间比面板上的红色状态更重要。常见失败包括检查命令不存在、URL 或端口写错、TLS/认证要求变化、依赖尚未就绪、只读文件系统阻止标记创建,以及检查超时低于正常冷启动时间。
第三步:在同一容器内重放检查
使用镜像里真实存在的工具执行健康命令,保留退出码:
docker exec app sh -c 'wget -qO- http://127.0.0.1/health; echo exit=$?'
docker exec app sh -c 'test -f /run/app/ready; echo exit=$?'
从宿主机访问映射端口只能证明外部路径;健康检查通常在容器网络命名空间内运行。反过来,容器内检查成功也不能替代负载均衡器的端到端检查,因此两条路径都要记录。
判断是错误探针还是应用未就绪
如果业务端点正常而健康命令引用了不存在的二进制或旧路径,应修复镜像/探针。如果进程存活但关键依赖、迁移或配置尚未完成,unhealthy 可能是正确保护信号;此时应修复就绪条件,而不是关闭探针。
健康端点应快速、有界,检查实例提供请求所必需的状态。把所有外部依赖都串行探测可能导致级联不健康;完全不检查关键依赖又会过早接流。需要根据流量切换和故障隔离目标定义条件。
最小修复
确认原因后只修改对应项:补齐检查工具、修正端口/路径、让应用在初始化完成后设置明确的就绪状态,或根据实测冷启动时间调整 start_period。修改 Dockerfile 或 Compose 后先在隔离环境验证:
docker compose config
docker compose up -d --wait
docker compose ps
不要只把 retries 改得很大;这会延迟真实故障被发现。也不要让健康检查执行昂贵查询或产生写入副作用。
复核与回滚
复核时确认同一个容器从 starting 转为 healthy,最近健康日志退出码为 0,外部请求正常,并观察超过一个检查周期。若修改后仍失败,恢复原镜像或 Compose 文件,再按变更前命令重建;保留失败容器日志和 inspect 输出用于复盘。
V2CE 复现实验记录
隔离实验启动 Nginx,主进程和默认 HTTP 页面正常,但健康检查额外要求 /tmp/ready。标记不存在时,Docker 记录 status=running running=true health=unhealthy,同一容器内 HTTP 页面仍返回 Nginx 欢迎页。创建预期的就绪标记后,没有重启进程,PID 保持不变,状态转为 health=healthy。
这证明运行状态、端点响应和探针就绪条件是三个不同证据。原始 inspect、健康日志与 Compose 状态保存在 labs/docker-healthcheck-unhealthy/evidence。