SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-07

Docker Compose healthcheck 如何正确编写

了解 Docker Compose 如何评估 healthcheck,为什么仅使用 depends_on 不会等待服务就绪,并掌握 Postgres 与应用的可靠 readiness 检查写法。

Docker Compose healthcheck 的实际作用

Docker Compose healthcheck 是 Docker 按固定时间间隔在容器内运行的一条命令。Docker 不会读取日志、监控端口或检查进程列表。它只运行该命令,读取退出码,并在容器上保存一个状态:startinghealthyunhealthy。退出码 0 表示健康。任何其他退出码都表示不健康。退出码 2 由 Docker 保留,因此不要主动返回该退出码。

这就是全部机制。几乎所有 healthcheck 问题都源于同一个原因:您编写的命令回答了另一个问题,而不是您真正想确认的问题。本指南假定您已经了解如何在 VPS 上编写 compose 文件,并从堆栈以错误顺序启动的位置继续说明。

services:
  api:
    image: ghcr.io/example/api:1.4.0
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s

test 值有两种常用形式。以 CMD 开头的列表会直接运行命令,不经过 shell,因此管道、&& 和变量展开都不会生效。以 CMD-SHELL 开头的列表会将其余内容作为一个字符串传递给容器内的 /bin/sh -c。只要检查需要 shell 语法,就应使用这种形式。纯字符串会被视为 CMD-SHELL。只包含 ["NONE"] 的列表会移除镜像通过 Dockerfile 内置的 healthcheck。

检查在容器内运行,因此命令中指定的每个二进制文件都必须存在于该镜像中。请先确认这一点。否则,缺少 curl 的精简镜像会导致容器永久处于不健康状态,而应用日志中不会显示原因。手动测试:

docker compose exec api curl --version

缺少二进制文件时会返回 OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown。基于 Alpine 的镜像通常会提供 BusyBox wget,因此检查命令应改为 ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"]

interval、retries 和 start_period 如何组合

有 5 个设置项控制检查时间。它们的默认值来自 Docker Engine,而不是 Compose。

  • interval:容器结束 start period 后,两次检查之间的时间间隔。默认值为 30s。
  • timeout:单次检查允许运行的最长时间。超过此时间后,Docker 会终止检查,并将本次检查计为失败。默认值为 30s。
  • retries:状态变为 unhealthy 前所需的连续失败次数。默认值为 3。
  • start_period:容器启动后的宽限期。默认值为 0s。
  • start_interval:start period 期间检查的运行间隔。默认值为 5s,且需要 Docker Engine 25.0 或更高版本。

需要注意的规则是:在 start period 期间,失败的检查不会计入 retries,容器会保持在 starting 状态。检查首次成功后,容器变为 healthy,并立即结束 start period,即使 start period 的大部分时间尚未用完。如果 start period 结束时检查仍然失败,则开始正常计数;容器必须连续失败 retries 次,才会被标记为 unhealthy

因此,从容器启动到变为 unhealthy 的最坏情况时间为 start_period 加上 retries 乘以 interval,再加上 timeout。使用上方文件中的值计算,即 30 加上 5 乘以 13,结果为 95 秒。设置部署超时时,先记下这个数字,因为 60 秒后就放弃的发布流程永远无法看到该容器进入最终状态。

这里常见的错误是提高 retries,以应对启动缓慢的问题。这样做只解决一次问题,之后会持续造成影响:某个服务启动时需要 8 次重试,生产环境现在会容忍连续 8 次失败,之后才会发现问题。应改用 start_period,因为它只在首次成功前生效。

为什么单独使用 depends_on 不能保证任何事情

depends_on 的短格式是大多数困惑的来源。

  api:
    depends_on:
      - db

这只表示一件事:先启动 db 容器,再启动 api 容器。Compose 会等待容器创建并启动,但不会等待 PostgreSQL 完成首次初始化,也不会等待 5432 端口接受连接。您的应用大约在 1 秒后启动,此时该端口还没有进程监听,应用于是退出。您会在日志中看到 Connection refused;如果服务器已经启动但仍在恢复,则会看到 FATAL: the database system is starting up

长格式才是实际需要的写法:

  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      migrate:
        condition: service_completed_successfully

condition 有 3 个值。service_started 与短格式相同。service_healthy 会让 Compose 暂缓启动依赖该服务,直到依赖项报告健康;只有当该依赖项在 compose 文件或其镜像中定义了 healthcheck 时,这个设置才有意义。service_completed_successfully 用于等待一次性容器退出,并确认其退出状态为 0,例如数据库迁移容器。

condition 旁边还有两个字段。restart: true 会让 Compose 在依赖服务更新后重启当前服务。required: false 会将缺少依赖项从错误降级为警告。

需要注意一个容易被忽略的限制。这些条件只会在启动整个堆栈时评估。它们用于控制启动顺序,不是监督规则。如果数据库在凌晨 3 点重启,系统不会重新评估 service_healthy,也不会为了再次满足该条件而重启您的应用。应用程序代码仍必须自行重新连接。docker compose up --no-deps api 会按设计跳过整个机制,直接使用 docker start 启动容器时也一样。

编写测试就绪状态的检查,而不是检查进程是否存在

类似 pgrep nginx 的检查只能证明进程表中存在一个条目。它无法证明服务能够响应请求。Web 应用即使数据库连接池已经失效,也可能长时间保持监听套接字处于打开状态;在整个故障期间,进程检查仍会显示正常。

让容器执行它所提供的服务:

  • 对于 HTTP 服务,请求一个真实端点。由于 -fcurl -fsS 在状态码为 400 或更高时会以非零状态退出,因此应用故障导致的 500 也会使检查失败。
  • 对于 PostgreSQL,使用 pg_isready。服务器接受连接时退出状态为 0,拒绝连接时为 1,完全无响应时为 2,传入的参数错误时为 3。
  • 对于 Redis,使用 redis-cli ping。该命令会输出 PONG,并以 0 状态退出。
  • 对于 MariaDB,官方镜像提供 healthcheck.sh 脚本,healthcheck.sh --connect --innodb_initialized 是其维护者文档中给出的用法。

pg_isready 有一个需要注意的问题。在空数据目录上首次启动时,官方 postgres 镜像会使用临时服务器执行初始化,该服务器只监听 Unix 套接字。不指定主机参数时,pg_isready 会使用这个套接字。因此,即使 TCP 端口 5432 对应用仍处于关闭状态,它也可能返回“接受连接”。将检查明确指向 TCP 即可解决问题,因为临时服务器不会在 TCP 上响应。

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

双美元符号不是笔误。Compose 读取文件时会自行展开 $VAR,这会将主机环境中的值写入检查命令。$$ 会将其转义为单个 $,这样容器内的 shell 就会根据容器自身的环境变量进行展开。

按正确顺序启动的 Postgres 和应用堆栈

services:
  db:
    image: postgres:17.5
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped

  api:
    image: ghcr.io/example/api:1.4.0
    environment:
      DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

volumes:
  pgdata:

启动堆栈并观察状态变化:

docker compose up -d
docker compose ps

STATUS列以方括号显示健康状态。运行正常的服务对会在两行中都显示 Up 41 seconds (healthy)。数据库仍在初始化时,db显示为 Up 4 seconds (health: starting),并且列表中没有 api,因为 Compose 尚未创建它。

要了解检查通过或失败的原因,请查看健康检查日志:

docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"

Docker 会保留最近几次结果。每条结果都包含开始时间、结束时间、一个 ExitCode,以及命令的 Output。保存的输出会被截断,因此输出大段页面内容的检查只会生成无用的日志条目。请让检查保持安静。

容器变为不健康时 Docker 会做什么

什么也不做。这是最让人意外的答案。

单主机上的 Docker Engine 不会重启不健康的容器。restart: unless-stopped 策略响应的是主进程退出,而不健康的容器并未退出。容器可以在 unhealthy 状态停留一周,而 Compose 不会处理它。Swarm 模式会替换不健康的任务,但在单台服务器上运行的普通 Compose 堆栈不会。

因此,实际可行的方案有两个。让进程在确认自身已故障时退出,使重启策略能够执行操作。或者从外部监控状态,并针对该状态触发告警。将 Uptime Kuma 监控器指向与健康检查调用的同一端点,可以让依赖项故障同时在两个位置显现;这样您会先从监控器得知问题,而不是由用户反馈。如果流量通过 Traefik 反向代理到达应用,请注意,代理对后端的自身判断与 Docker 健康状态相互独立,因此两者不能互相替代。

排查始终无法变为 healthy 的检查

在同一个容器中亲自运行完全相同的命令,并查看退出码:

docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"

容器仍报告 unhealthy,但此处为 exit=0,说明 compose test 与您刚才输入的内容不同。通常,这是因为使用了 CMD,而该位置需要 shell 语法。

其余问题大多来自两种错误。第一种是端口错误。healthcheck 在容器内部运行,因此必须使用容器端口,不能使用发布到主机的端口。使用 ports: - "8080:3000" 时,应用监听 3000 端口;如果检查使用 http://localhost:8080,就会一直失败,但网站在浏览器中运行正常。第二种是主机错误。在检查命令中,localhost 表示当前容器。检查当前容器时这样写是正确的,但检查邻近容器时就不对了。此时应使用服务名称,例如 db

还有一种情况需要单独说明:healthcheck 通过了,但用户仍看到错误。这通常是因为端点只返回静态的 200 响应,没有实际访问任何依赖项。一个从不查询数据库的就绪端点无法发现数据库已经不可用。应让它执行一次开销较低的实际查询。

FAQ

为什么 depends_on 显示数据库运行状况良好,但我的应用仍无法连接?

因为 condition: service_healthy 只会在堆栈启动时评估一次。之后它不会继续监控任何内容。如果数据库容器稍后重启,Compose 不会为了再次满足该条件而重启应用,因此应用代码需要自行实现重新连接和重试逻辑。使用 docker startdocker compose up --no-deps 启动单个容器时,该条件也不会生效。

如果镜像已经定义了 healthcheck,是否还需要再定义?

通常不需要。覆盖现有检查往往会降低可靠性,因为镜像维护者更清楚该软件的就绪状态应如何判断。只有当镜像中的检查不适合您的环境时,才应添加自定义检查,例如镜像检查的是您已更改的端口。要禁用镜像中的 healthcheck,请在服务上设置 test: ["NONE"]disable: true

healthcheck 应使用 curl 还是 wget?

使用镜像中已经存在的工具,并在依赖它之前通过 docker compose exec <service> curl --version 确认。许多基于 Debian 的镜像两者都没有。基于 Alpine 的镜像提供 BusyBox wget。如果软件自带客户端(例如 pg_isreadyredis-cli),不要仅为了运行 healthcheck 而向镜像中添加软件包。

不健康的容器会自动重启吗?

在单主机环境中,Docker Engine 不会自动重启。重启策略响应的是进程退出,而不是 health 状态。因此,不健康的容器会继续运行并保持故障状态,直到其他机制采取措施。您可以让进程检测到故障后退出,也可以运行外部监控程序,在状态异常时发送告警。

start_period 应设置多长?

应至少覆盖您测量到的最慢正常首次启动时间,并留出一定余量。使用 docker compose up 在空卷上进行计时,因为数据库首次启动的速度远慢于后续启动。start_period 过长,只会延迟首次 unhealthy 判定。重试次数过高会削弱容器整个生命周期内的检查,这是更严重的问题。

#docker-compose#healthcheck#depends-on#Docker#reliability