发布时间:2026-09-21 04:30 更新时间:2026-09-21 04:30 阅读量:0
不少站长都遇到过这种场景:docker ps 里容器状态是 Up,进程也在,但打开网站就是 502,或者接口一直转圈。登进去一看,容器里的 PHP-FPM、Node 或 Nginx worker 早就卡死了,只是主进程还挂在那里没有退出。Docker 默认只看主进程(PID 1)是否存活,进程不退出它就认为容器健康,于是重启策略也不会触发。要解决这个问题,需要让 Docker 自己去做业务层探测,也就是本文要讲的 HEALTHCHECK。
HEALTHCHECK 是 Dockerfile 里的一个指令,它让 Docker 以固定间隔在容器内执行一条命令,用命令的退出码判断容器状态:返回 0 表示 healthy,返回 1 表示 unhealthy,其他退出码按 1 处理。它和 docker ps 显示的 Up 是两回事,Up 只代表主进程活着,健康状态则另有一列展示。
在 Dockerfile 中的基本写法如下,注意 CMD 后面的命令必须能在容器内执行,探测方式不要依赖容器里没有的工具:
FROM nginx:stable
用容器内自带的工具探测,避免额外安装依赖
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS http://127.0.0.1/healthz || exit 1
四个参数的含义需要分清。--interval 是两次探测的间隔,默认 30s;--timeout 是单次探测的超时时间,默认 30s,超过就按失败算;--start-period 是容器启动后的宽限期,这段时间内探测失败不计入失败次数,适合启动慢的应用;--retries 是连续失败多少次才把状态标记为 unhealthy,默认 3 次。这几个值加起来决定了从服务出问题到被判定 unhealthy 的总时长,粗略等于 interval 乘以 retries,再考虑 timeout 的影响,具体以官方文档和实际环境为准。
如果镜像已经构建好了不想改 Dockerfile,可以在运行时用 --health-cmd 等参数覆盖,查看状态则用 docker inspect:
# 运行时追加健康检查
docker run -d --name web \
--health-cmd="curl -fsS http://127.0.0.1/healthz || exit 1" \
--health-interval=30s --health-timeout=3s --health-retries=3 \
nginx:stable
只看健康状态,输出如 healthy / unhealthy / starting
docker inspect --format '{{.State.Health.Status}}' web
看最近几次探测的详细输出,排查探测命令本身是否写错
docker inspect --format '{{json .State.Health}}' web
这里有个常见坑:探测命令写得太“轻”,比如只判断端口能不能连上,而应用卡死在业务逻辑里端口依然在监听,那就探不出来。更靠谱的做法是探一个真实的健康接口,让它顺带检查依赖,例如数据库连接、Redis 连通性。反过来说,探测命令也不要写得太重,否则间隔一短就会给服务增加额外负载。
实际项目里更多是用 Docker Compose 编排,此时 healthcheck 写在服务下面,语法和 Dockerfile 一致,只是参数名换成小写。配合 depends_on 的 condition 还能做到“等数据库真正就绪再启动应用”,避免应用先起来连不上库而反复崩溃。
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: example
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pexample"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/healthz || exit 1"]
interval: 30s
timeout: 3s
retries: 3
start_period: 20s
注意两点:test 用数组形式时不会经过 shell,管道、重定向这类写法要用 CMD-SHELL;另外 depends_on 的 condition 只在 compose 启动阶段生效,运行中某个服务变成 unhealthy 并不会自动去重启它,这一点很多人会误解。
Docker 本身并不会因为健康检查失败就去重启容器,--restart=always 只在容器退出时生效。所以要做自动恢复,通常有两条路:一是让容器在探测失败时主动退出,由重启策略接管;二是用外部脚本或工具监听健康状态并执行重启。
比较省事的是在应用内部加一个自愈逻辑,探测失败就退出主进程。例如写一个探测脚本,失败时 kill 掉 PID 1,容器退出后由 --restart=unless-stopped 拉起:
#!/bin/sh
health-watch.sh:探测失败则退出主进程,交给 Docker 重启策略
while true; do
if ! curl -fsS http://127.0.0.1:8080/healthz >/dev/null 2>&1; then
echo "health check failed, exiting..." >&2
kill -TERM 1
exit 1
fi
sleep 30
done
另一种是外部轮询,用脚本定期检查所有容器的健康状态,发现 unhealthy 就重启。这种方案不改镜像,适合存量环境,但要注意加日志和限流,避免服务刚重启还没起来又被判定失败、陷入反复重启:
#!/bin/bash
遍历所有容器,把 unhealthy 的重启一次
for c in $(docker ps --format '{{.Names}}'); do
status=$(docker inspect --format '{{.State.Health.Status}}' "$c" 2>/dev/null)
if [ "$status" = "unhealthy" ]; then
echo "$(date '+%F %T') restart $c"
docker restart "$c"
fi
done
把这个脚本交给 crontab 或 systemd timer 定期执行即可。不过更成熟的运维方式是用容器编排平台自带的自愈能力,比如 Kubernetes 的 livenessProbe 会在探测失败后重启 Pod,Swarm 也支持基于健康状态的任务重建,具体行为以对应平台的官方文档为准。无论用哪种方式,都建议给重启动作加上次数限制或退避策略,并保留日志,否则一个配置错误可能让容器无限重启,把问题掩盖掉。
最后提醒一句,健康检查是“发现问题的哨兵”,不是“解决问题的万能药”。如果容器频繁变成 unhealthy,应该先通过 docker inspect 里最后一次探测的输出、以及应用自身日志去定位根因,是内存不够、依赖服务挂了,还是探测命令本身写错了,而不是简单地把重启频率调高。把探测接口、超时和重试参数按业务实际响应时间来设定,再配合合理的重启策略,容器里的假死服务才能真正被自动拉回来。
| 📑 | 📅 |
|---|---|
| Nginx location 匹配优先级实战:=、^~、~ 命中顺序验证 | 2026-09-21 |
| Docker容器缺命令:精简镜像补装与临时容器调试 | 2026-09-20 |
| Nginx 上传大文件报 413 与超时:参数与缓冲区排查 | 2026-09-20 |
| 服务器网卡多队列与中断绑定入门:RPS、RSS 与 irqbalance 怎么取舍 | 2026-09-20 |
| Linux时间与时区排查:date、timedatectl与容器时区一致性 | 2026-09-20 |
| MySQL索引失效排查:EXPLAIN执行计划与隐式转换定位 | 2026-09-21 |
| systemd-resolved 与 /etc/resolv.conf 冲突排查 | 2026-09-21 |
| Nginx与PHP上传目录权限:www-data、umask与0777的坑 | 2026-09-21 |
| MySQL 授权与远程访问配置实战:user@host 匹配规则、bind-address 与防火墙三层放行检查 | 2026-09-21 |
| Docker容器时间漂移与crond定时任务错乱:TZ与宿主时间源协同 | 2026-09-21 |