先做预测
启动后仍会等待
容器主进程在运行,不代表服务能处理请求。数据库可能还在建数据目录、加载数据或打开监听端口。“就绪”指它已能接受你需要的请求。
这里的 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。其他镜像需换成对应工具。配置语法见官方参考。
- Compose 按依赖关系先启动 db。
- db 主进程运行,健康状态是 starting。先猜:此时会启动 app 吗?
- 不会。探针检查失败,app 继续等待。这里假设 db 在宽限期内完成初始化。
- 后续探针成功,健康状态变成 healthy,Compose 观察到这个结果。先猜:现在 app 的启动门槛满足了吗?
- 满足。Compose 现在才启动 app。
- app 连接已就绪的 db。成功仍以配置正确、网络可达且 db 保持可用为前提。
| 字段 | 上例的含义 |
|---|---|
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 不会按这个条件正常放行。修好后重新启动并确认状态;不要把这理解成“无论失败多久都会一直等”。
不看上面的 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. 用短写法启动
预期(未实测):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 秒。
预期(未实测):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。它删除本项目容器和默认网络;实验文件与已拉取的镜像保留。实验没有创建数据卷。
换一种故障再判断
启动门槛有边界
启动门槛只解决启动时的等待。应用已经运行后,数据库仍可能断开、重启或失去响应。应用应实现连接重试与重连,并设置失败上限,避免无限卡住。
探针成功也只证明你检查的事情通过。端口能连接,不保证账号可用、业务表齐全或跨容器网络正常;单纯 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 长写法。
合上配置再回忆
用证据排查
- 启动顺序看
depends_on,就绪等待看service_healthy与探针。 - 探针在被检查容器内运行,工具必须存在,检查必须有意义。
- 出问题先看所有容器的状态与探针记录,再区分启动故障和运行期故障。
app 已经过健康门槛并运行了半小时。随后 db 仍在运行,但变为 unhealthy。仅有本页的 depends_on 配置,哪项判断正确?
启动时的依赖等待没有定义运行中自动停止与恢复的控制流程。
unhealthy 是健康状态。它本身不会让仍在运行的主进程退出,也不会触发自动重启。
健康检查持续提供状态,depends_on 的启动门槛已完成本次职责。用探针记录定位 db 为什么失败,同时确认 app 能否在故障后重试与重连。
来源
核对日期:2026-11-03。配置语义以 Docker 官方文档为准;实验输出待维护者实测补充。
- Control startup and shutdown order in Compose:运行与就绪的区别、健康依赖及 PostgreSQL 探针示例。
- Compose services · depends_on 与 healthcheck:短长写法、条件与探针语法。
- Dockerfile · HEALTHCHECK:退出码、状态、宽限期与检查日志。
- docker run、docker inspect:临时容器与格式化查询。
- docker compose up 与 down:后台启动、资源清理范围。
- PostgreSQL · pg_isready:连接状态检查、退出码及检查范围;BusyBox 官方镜像:实验镜像标签;BusyBox 命令参考:httpd、wget 选项。