Docker 容器启动成功并不代表服务已经就绪。比如一个 Java 应用可能在监听端口前还要完成几十秒的初始化,如果直接把容器标记为运行中,上层负载均衡或编排系统就会在服务不可用时把流量打进来。Dockerfile 中的 HEALTHCHECK 指令就是用来解决这个问题的:它让容器周期性地执行一条检查命令,根据命令退出码判断容器是否真正健康。本文会从指令语法、参数细节和常见坑位几个角度把 HEALTHCHECK 的用法讲透。

指令基础:CMD 与 NONE 两种写法
HEALTHCHECK 指令有两种基本形式:HEALTHCHECK CMD 和 HEALTHCHECK NONE。前者用于定义一条具体的健康检查命令,后者用于显式禁用从基础镜像继承过来的健康检查。如果 Dockerfile 中既没有出现 HEALTHCHECK CMD,也没有出现 HEALTHCHECK NONE,那么该镜像会继承基础镜像中定义的 HEALTHCHECK;如果基础镜像也没有,则容器没有任何健康检查。
HEALTHCHECK CMD 后面的命令可以写成 exec 数组形式,也可以写成 shell 字符串形式。exec 数组形式不会启动额外的 shell 进程,推荐在生产环境中使用;shell 字符串形式则会在 /bin/sh -c 中执行,适合一些简单命令。下面是一个使用 curl 检查 Nginx 服务的 Dockerfile 示例:
FROM nginx:alpine RUN apk add --no-cache curl HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD curl -f https://www.xiadaxia.com.cn/ || exit 1
在这个例子里,容器启动后会每隔 30 秒执行一次 curl -f https://www.xiadaxia.com.cn/。如果 curl 返回非 0 退出码,命令会显式返回 1,表示不健康。只有当连续 3 次检查都失败时,容器才会被标记为 unhealthy;如果中途有一次成功,计数会重置。这种重试机制可以避免瞬时抖动导致误报。
如果基础镜像已经自带了健康检查,而你的应用又不想使用它,可以在 Dockerfile 中写 HEALTHCHECK NONE。这个指令不会创建任何检查,也不会覆盖其他指令,只是单纯禁用继承。需要注意的是,HEALTHCHECK NONE 不能通过命令行为容器临时启用健康检查,因为 Dockerfile 中的 NONE 会覆盖 docker run --health-cmd 中指定的命令。
参数细节与状态转换机制
HEALTHCHECK 指令支持六个可选参数,它们都放在 CMD 之前,以 -- 开头。这些参数可以直接写在 Dockerfile 中,也可以在命令行中通过 docker run --health-* 覆盖。下面是各参数的含义和默认值:
| 参数 | 默认值 | 作用 |
|---|---|---|
| --interval | 30s | 两次健康检查之间的时间间隔 |
| --timeout | 30s | 单次检查命令的超时时间,超过则判定失败 |
| --retries | 3 | 连续失败多少次才将容器标记为 unhealthy |
| --start-period | 0s | 容器启动后的缓冲期,此期间检查失败不计入重试次数 |
| --start-interval | 5s | 缓冲期间执行检查的时间间隔,需 Docker 25.0+ |
健康检查的退出码决定了容器状态:0 表示成功,容器进入 healthy;1 表示失败,如果连续失败次数达到 retries,容器会变为 unhealthy;2 是保留值,不应在检查命令中使用。在容器刚启动且还没有完成第一次检查时,状态显示为 starting。如果 start-period 设置了非零值,在该时间段内即使检查失败,也只会显示 starting,不会进入 unhealthy,这给慢启动的应用留出了缓冲。
这里有一个容易混淆的地方:timeout 是单次命令的执行超时,interval 是两次命令之间的间隔,而 retries 是连续失败次数阈值。假设 interval=30s、timeout=3s、retries=3,那么最坏情况下容器会在 3 次失败后,即第 3 次执行完毕(约 30+3秒?实际上间隔从上次执行结束开始计算)被标记为 unhealthy。通常从第一次失败到进入 unhealthy 大约要 36 秒以上。
如果需要在缓冲期内更频繁地检查,可以使用较新的 --start-interval 参数,它只在 start-period 内生效。例如一个数据库镜像可能需要 60 秒启动,可以这样写:
FROM postgres:16 HEALTHCHECK --start-period=60s --start-interval=5s \ --interval=30s --timeout=5s --retries=5 \ CMD pg_isready -U postgres || exit 1
这段配置会在容器启动后的前 60 秒内每 5 秒检查一次,启动完成后恢复到每 30 秒检查一次。合理的 start-period 可以避免在数据库初始化阶段被误判为 unhealthy,进而触发编排系统的重启或摘除流量。
避坑指南:常见错误与排查
第一个常见错误是使用 shell 字符串形式执行包含变量或管道的复杂命令时,没有注意到 shell 进程的存在。例如 HEALTHCHECK CMD curl -f https://www.xiadaxia.com.cn/health | grep -q "ok" 这种写法,管道符号会在 /bin/sh -c 中解释,但退出码是 grep 的退出码,如果 grep 匹配不到会返回 1,这通常符合预期;但如果命令中混入了后台进程或引号嵌套,就很容易出现意外行为。因此更推荐把检查逻辑写进脚本文件,然后在 Dockerfile 中调用。
第二个坑是检查命令本身过重。健康检查应该尽量轻量,只做存活探测或简单的就绪探测。比如对一个 Web 服务,用 curl -f 打本地健康端点就够了,不要跑到容器外去请求外部 API,否则可能因为网络波动或外部服务不可用导致容器被误杀。同时检查命令执行时间不能超过 timeout,否则会被强制判定失败,所以在脚本里要避免执行耗时的同步操作。
第三个坑是忽略了基础镜像自带的 HEALTHCHECK。比如某些官方镜像如 redis 或 mariadb 可能内置了健康检查,如果你在派生镜像中重新定义了检查逻辑,旧逻辑会被覆盖;但如果你的应用需要不同的检查方式而忘了写 HEALTHCHECK,反而会继承一个不适合的检查命令。排查这类问题时,可以用 docker inspect --format='{{json .Config.Healthcheck}}' 镜像名 查看最终生效的健康检查配置。
还有一个值得注意的地方:HEALTHCHECK NONE 并不是删除健康检查,只是禁用。在已经配置 HEALTHCHECK CMD 的镜像中,如果想让某个容器不执行健康检查,最好的方式是在 docker run 时使用 --no-healthcheck 参数,而不是修改 Dockerfile 为 NONE,因为修改 Dockerfile 会影响所有使用该镜像的容器。
实战:为典型服务添加健康检查
以 Nginx 静态站点为例,最简单的健康检查是请求本地首页,只要 HTTP 返回 200 就认为健康。但有些场景下首页返回 200 不代表后端服务可用,这时可以单独准备一个健康端点,例如 /healthz,由应用返回 200 或 204。Dockerfile 可以这样写:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=20s --timeout=3s --retries=3 \
CMD node -e "fetch('http://localhost:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.js"]
这个检查利用 Node.js 内置的 fetch 请求健康端点,不需要额外安装 curl 或 wget。如果服务本身是用 Node.js 写的,这样做还能保持镜像体积较小。注意代码中的双引号和箭头函数在 Dockerfile 的 shell 字符串中可能被转义或解释,因此更推荐把检查逻辑放进单独的可执行脚本,例如 healthcheck.js,然后在 Dockerfile 中直接执行 CMD node healthcheck.js。
对于数据库类服务,官方往往提供了专门的检查工具。例如 PostgreSQL 提供 pg_isready,MySQL 提供 mysqladmin ping。使用这些工具可以更准确地判断服务是否真正接受连接,而不是仅仅检查进程是否存在。下面是一个 MySQL 8 的健康检查示例:
FROM mysql:8.0 ENV MYSQL_ROOT_PASSWORD=rootpass HEALTHCHECK --interval=10s --timeout=5s --retries=5 --start-period=30s \ CMD mysqladmin ping -h localhost -uroot -p$MYSQL_ROOT_PASSWORD --silent || exit 1
这里设置了 30 秒的启动缓冲,因为 MySQL 首次初始化数据目录需要时间。在生产环境中,密码不应该硬编码在 Dockerfile 中,可以通过环境变量注入,但要注意健康检查命令中的变量会在容器运行时展开。如果检查命令涉及敏感信息,建议使用 Docker secrets 或读文件的方式获取密码,避免明文出现。
最后,健康状态不仅可以通过 docker ps 查看,还能在 Docker Compose 文件中用 healthcheck 字段定义,并配合 depends_on 的 condition: service_healthy 实现依赖等待。这样启动顺序就不仅依赖容器启动,而是等到服务真正健康后才继续启动下游容器。虽然 Docker Compose v2 已不再强制要求使用 depends_on,但在多服务编排中,显式声明健康依赖仍然是减少启动故障的有效手段。
总体而言,HEALTHCHECK 是一个简单但非常实用的指令。把健康检查固化到 Dockerfile 中,不仅能提升容器的可观测性,还能让 Swarm、Kubernetes 等编排系统更准确地管理容器生命周期。只要注意命令的轻量性、超时时间与启动缓冲的配合,再避免继承和覆盖带来的混乱,就能把容器健康检查配置得既可靠又省心。

