← 全部笔记

Docker · 6 Compose 多容器

依赖与启动顺序:
depends_on 与健康检查

先启动数据库,应用仍可能连接失败。要等到可用,需要把“就绪”变成可检查的条件。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · 实验未实测 · 面向 Compose 2.40.3

只写 depends_on 时的一次启动竞态 时间向下。Compose 先启动数据库,再启动应用。应用连接时数据库仍在初始化,连接失败;数据库之后才就绪。 Compose db app 启动 db 启动 app 仍在初始化 连接失败 此时才就绪
图 1 短写法只保证启动顺序,数据库初始化期间仍可能拒绝连接。时间向下,间距为示意。

先做预测

启动后仍会等待

容器主进程在运行,不代表服务能处理请求。数据库可能还在建数据目录、加载数据或打开监听端口。“就绪”指它已能接受你需要的请求。

这里的 db 是数据库服务,app 是使用数据库的应用。depends_on 写在依赖方 app 下,表示先启动它需要的 db。官方启动顺序说明明确区分了运行与就绪。

# app 服务内的片段;镜像等字段省略
depends_on:
  - db

先看图 1:只写上面的短写法,db 需要初始化,app 启动后只连接一次。你能保证 app 成功吗?写出原因再揭晓。

不能保证。Compose 已满足“db 先启动”,但 app 的连接可能落在初始化期间。也可能由于调度或 app 自身启动较慢而成功;偶尔成功不能证明配置会等待就绪。

这种结果取决于谁先完成的情况叫竞态(race condition)。把 db 写在 YAML 的前面,或让 app 固定等几秒,都没有定义可用条件。

需要回忆 Docker 的运行链路,可回看已有的第一次 docker run。本页只处理启动依赖;完整 Web 与数据库应用留到下一页。

把就绪变成信号

健康后再放行

两处配置缺一不可:db.healthcheck 定义怎么检查,app.depends_on.db.condition 定义等什么。健康检查(healthcheck)是在被检查容器内反复执行的命令,也叫探针。

# 两个服务内的配置片段,尚不是完整实验文件
services:
  app:
    depends_on:
      db:
        condition: service_healthy
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U demo -d demo || exit 1"]
      interval: 2s
      timeout: 1s
      retries: 10
      start_period: 20s

这段探针用于提供 pg_isready 工具和 shell 的 PostgreSQL 数据库镜像;demo 是示例用户和库名。它检查数据库是否接受连接,127.0.0.1 指向 db 容器自己。成功返回 0;|| exit 1 把工具的各种失败统一为 1,避免使用健康检查保留的退出码 2。其他镜像需换成对应工具。配置语法见官方参考。

service_healthy 等待健康检查通过的六步时序 Compose 启动 db,db 初始化时检查失败,app 尚未启动。探针成功后报告 healthy,Compose 再启动 app,app 连接 db。图中间隔不表示耗时。 Compose db app 启动 db 初始化中 starting 尚未启动 检查失败 继续等待 检查成功 healthy 报告健康 门槛通过,启动 app 连接成功
  1. Compose 按依赖关系先启动 db。
  2. db 主进程运行,健康状态是 starting。先猜:此时会启动 app 吗?
  3. 不会。探针检查失败,app 继续等待。这里假设 db 在宽限期内完成初始化。
  4. 后续探针成功,健康状态变成 healthy,Compose 观察到这个结果。先猜:现在 app 的启动门槛满足了吗?
  5. 满足。Compose 现在才启动 app。
  6. app 连接已就绪的 db。成功仍以配置正确、网络可达且 db 保持可用为前提。
图 2 service_healthy 把探针通过放在 app 启动之前。时间向下;这是顺序示意,不代表真实耗时。
字段上例的含义
test容器内执行的检查。CMD 直接运行参数列表;CMD-SHELL 才使用 shell。
interval: 2s常规阶段,上一次检查结束后再等 2 秒。
timeout: 1s单次检查超过 1 秒,算失败。
retries: 10计入的连续失败达到 10 次,标记 unhealthy;检查仍会继续。
start_period: 20s启动宽限期内的失败不计入阈值;提前成功仍可立刻变 healthy。

这些值是教学配置,并非默认值。健康状态独立于运行状态:starting 是检查尚未通过,healthy 是检查成功,unhealthy 是连续失败达到阈值。状态转换规则见 Dockerfile 参考。

如果 db 达到 unhealthy,当前启动操作会报告依赖失败,app 不会按这个条件正常放行。修好后重新启动并确认状态;不要把这理解成“无论失败多久都会一直等”。

图 3 探针决定信号的含义,Compose 只按这个信号放行。

不看上面的 YAML:app 要等待 db 的健康信号,该填哪个条件?

depends_on.db.condition:

service_healthy 才等待健康检查。只给 db 添加探针,app 仍用短写法,启动门槛就没有改变。

db 已定义正确探针,但 app 仍写 depends_on: [db]。哪项能保证 app 等到探针通过?

YAML 书写位置不定义就绪条件,依赖关系才参与排序。

retries 改变失败阈值,没有改变 app 等待的条件。

探针负责产出健康状态,condition 负责消费健康状态。必须把两者连起来,才会推迟 app 的启动。

深入一点:宽限期不是固定等待

start_period 期间仍会检查。期间第一次成功就会变 healthy;之后连续失败开始计数。宽限期的检查间隔可用 start_interval 设置;上例未设置,使用其默认值 5 秒。不要把 start_period + retries × interval 当作精确启动时限,检查耗时和提前成功都会影响结果。

需要变量展开时,可写 test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} || exit 1"]。双美元符号让 Compose 把美元符号留给容器内的 shell。pg_isready 不验证业务表是否存在;需要时,探针应执行能代表业务就绪的查询。

在自己机器上观察

改门槛看差异

只改变启动条件,比较 app 何时发请求。实验把服务命名为 db,但它是延迟开放端口的 HTTP 模拟服务,不是真数据库。这样无需数据库账号和数据卷,就能观察初始化窗口。

未实测

本页面编写环境没有 Docker。下面都是待维护者补测的实验与预期,未展示实测终端输出。你需要可用的 Docker Engine 和 Compose;在宿主机的 Linux shell 或 WSL 中运行。

实验会拉取镜像、创建容器和 Compose 默认网络;Docker 会按自身配置管理该网络及相关主机网络规则。无需手动改 iptables、发布宿主机端口或启用特权。文件写在新建的实验目录内。

1. 先观察“进程运行”和“准备完成”

这个小命令刻意等 3 秒,帮助你观察运行中的进程仍在准备工作。--rm 在退出后删除容器;镜像保留。

docker run --rm busybox:1.37 sh -c \
  'printf "process running\n"; sleep 3; printf "ready\n"'

预期(未实测):首次可能先出现镜像拉取信息,随后是命令中明确写出的两行文本 process running、ready,两行相隔约 3 秒。这些文本是实验标记,不是 Docker 自带状态。

2. 保存完整 compose.yaml

mkdir compose-ready-lab
cd compose-ready-lab
cat > compose.yaml <<'YAML'
services:
  db:
    image: busybox:1.37
    command:
      - sh
      - -c
      - |
        mkdir -p /www
        printf 'ready\n' > /www/index.html
        sleep 12
        exec httpd -f -p 8080 -h /www
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/"]
      interval: 2s
      timeout: 1s
      retries: 10
      start_period: 20s
  app:
    image: busybox:1.37
    command: ["sh", "-c", "wget -q -O - http://db:8080/ && printf 'app connected\n'"]
    depends_on:
      - db
YAML
docker compose -p ready28 config
docker compose -p ready28 pull

sleep 12 专门制造初始化窗口;真实应用不该靠它等待依赖。实验使用 BusyBox 官方镜像;httpd 是镜像内的 HTTP 服务程序;wget 发起请求。db 的探针从容器内部检查自己的端口,app 则用同一网络上的服务名 db 访问它。

config 检查并展开配置;预期(未实测)输出规范化 YAML,若失败先修正缩进。pull 提前拉取镜像,减少下载造成的时序干扰。后续命令始终带 -p ready28,把资源归入同一实验项目。

3. 用短写法启动

docker compose -p ready28 up -d
docker compose -p ready28 ps -a
docker compose -p ready28 logs app

预期(未实测):app 往往在 12 秒初始化窗口内发请求,连接失败并非零退出。具体错误文案不在此预设。即使稍后 db 已 healthy,app 的这次请求也不会自动重试;ps -a 才会包含退出的 app。

如果 app 成功,可能是它实际发请求时 db 已准备好。先执行下面的 down,再把 sleep 12 改长后复跑。竞态意味着可能失败,不意味着每次必然失败。

4. 换成长写法,再从头启动

docker compose -p ready28 down

先清掉上一轮的容器和默认网络。然后在 compose.yaml 中,仅把 app 的整个 depends_on 块替换为:

    depends_on:
      db:
        condition: service_healthy

db 的 sleep 和探针都没变。预测:app 会在 db 初始化期间发请求,还是在第一次健康检查成功后?你将用什么证据判断?

第一次探针成功后才启动 app。观察 up 等待健康状态,再看 app 日志与退出码;db 正在初始化时,app 不应开始执行请求。实际等待时间取决于探针调度,不必刚好等于 12 秒。

docker compose -p ready28 config
docker compose -p ready28 up -d
docker compose -p ready28 logs app
docker compose -p ready28 ps -a
docker inspect --format '{{.State.Health.Status}}' "$(docker compose -p ready28 ps -q db)"
docker inspect --format '{{json .State.Health.Log}}' "$(docker compose -p ready28 ps -q db)"

预期(未实测):up 在 db 健康后放行 app。app 日志包含脚本定义的 ready 与 app connected;app 正常退出(退出码 0),因为它只发一次请求,不是常驻应用。健康状态查询预期为 healthy,探针记录可用于查看检查时间、退出码与输出。

如果没通过:先看探针记录。命令不存在、检查地址错、服务未监听,都会让探针失败。用 docker compose -p ready28 exec db wget -q -O - http://127.0.0.1:8080/ 手动复现检查;预期成功时得到脚本写入的 ready。不要用不断增加 retries 掩盖探针错误。

最后清理:在实验目录运行 docker compose -p ready28 down。它删除本项目容器和默认网络;实验文件与已拉取的镜像保留。实验没有创建数据卷。

换一种故障再判断

启动门槛有边界

启动门槛只解决启动时的等待。应用已经运行后,数据库仍可能断开、重启或失去响应。应用应实现连接重试与重连,并设置失败上限,避免无限卡住。

db检查未通过 门槛未通过 app尚未启动 先查探针,再查初始化
图 4 初次启动时,健康条件未满足会阻止 app 启动。
dbunhealthy 请求可能失败 app仍可运行 应用负责重试与重连
图 5 运行中变 unhealthy,不会仅因此触发依赖方停止或重启。

探针成功也只证明你检查的事情通过。端口能连接,不保证账号可用、业务表齐全或跨容器网络正常;单纯 test: ["CMD", "true"] 更不能代表数据库就绪。

同事把探针改成永远返回 0,app 果然启动了。用两句话解释:为什么原来的竞态仍可能发生?

这个探针不观察服务就绪,数据库还没准备好也会报告 healthy。Compose 会按这个假信号放行 app,原来的初始化窗口仍在。

  • 探针成功只代表检查命令成功
  • 检查内容必须覆盖应用需要的就绪条件
深入一点:其他 condition 与 restart

service_started 等价于短写法;service_completed_successfully 等待一次性任务成功退出,例如初始化任务。这和等待长期运行的数据库变健康是不同门槛。

depends_on.db.restart: true 用于显式 Compose 更新或重启依赖后,带着重启依赖方。它不覆盖容器崩溃后的运行时自动重启,也不把 unhealthy 变成自动修复动作。服务级 restart 策略又是另一个配置;不要混在一起理解。参见depends_on 长写法。

合上配置再回忆

用证据排查

app 已经过健康门槛并运行了半小时。随后 db 仍在运行,但变为 unhealthy。仅有本页的 depends_on 配置,哪项判断正确?

启动时的依赖等待没有定义运行中自动停止与恢复的控制流程。

unhealthy 是健康状态。它本身不会让仍在运行的主进程退出,也不会触发自动重启。

健康检查持续提供状态,depends_on 的启动门槛已完成本次职责。用探针记录定位 db 为什么失败,同时确认 app 能否在故障后重试与重连。

来源

核对日期:2026-11-03。配置语义以 Docker 官方文档为准;实验输出待维护者实测补充。

  1. Control startup and shutdown order in Compose:运行与就绪的区别、健康依赖及 PostgreSQL 探针示例。
  2. Compose services · depends_on 与 healthcheck:短长写法、条件与探针语法。
  3. Dockerfile · HEALTHCHECK:退出码、状态、宽限期与检查日志。
  4. docker run、docker inspect:临时容器与格式化查询。
  5. docker compose up 与 down:后台启动、资源清理范围。
  6. PostgreSQL · pg_isready:连接状态检查、退出码及检查范围;BusyBox 官方镜像:实验镜像标签;BusyBox 命令参考:httpd、wget 选项。