在多容器项目中,最常见的启动异常之一,就是应用容器已经运行,数据库、缓存或消息队列却仍在初始化,最终出现“Connection refused”“启动后立即退出”等问题。🔍 很多人以为配置了 depends_on 就能等待依赖服务完全可用,实际上它默认主要解决容器的创建与启动顺序,并不等同于业务服务已经就绪。本文将从条件依赖、健康检查和诊断命令三个方面,梳理一套可直接执行的排查方法。
一、先区分“容器启动”和“服务就绪”
Docker Compose 启动服务时,会根据 depends_on、网络模式等依赖关系安排顺序。短格式配置可以保证被依赖容器先启动,但 Compose 默认只确认容器进入运行状态,不会自动判断 PostgreSQL 是否可以执行查询、Redis 是否能够响应命令,或者 HTTP 接口是否已经完成初始化。相关行为可参考 Docker 官方启动顺序说明。
services:
app:
depends_on:
- db
db:
image: postgres
上述配置表示先启动 db,再启动 app。如果数据库初始化需要数秒,app 仍可能在数据库可连接之前发起请求。因此,日志中的连接失败不一定意味着启动顺序完全失效,更可能是“容器已启动,但服务尚未就绪”。⚠️
二、正确使用 depends_on 条件依赖
需要等待依赖服务满足特定状态时,应使用 depends_on 的长格式。常见条件包括以下三种:
- service_started:依赖容器已经启动,效果接近短格式。
- service_healthy:依赖服务的健康检查已经通过。
- service_completed_successfully:依赖容器作为一次性任务运行,并以成功状态结束。
对于“应用依赖数据库”场景,通常应选择 service_healthy;对于数据库迁移、配置初始化等一次性任务,则适合使用 service_completed_successfully。✅
services:
app:
build: .
depends_on:
db:
condition: service_healthy
db:
image: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 3s
retries: 10
start_period: 15s
这里的关键点是:只有数据库健康检查成功后,app 才会进入启动流程。命令中的双美元符号用于避免变量在 Compose 解析阶段被提前替换,使其留到容器内部执行。
三、健康检查为什么会误判
健康检查并非配置上就一定可靠。最常见的问题是检查命令在镜像中不存在。例如,用精简镜像运行 HTTP 服务时,容器里可能没有 curl;MySQL、PostgreSQL 或 Redis 镜像的检查工具和参数也可能因镜像版本、用户配置而不同。
另一个误区是只检查进程或端口。端口已经监听,不代表数据库迁移完成,也不代表应用接口具备处理请求的能力。更稳妥的做法是使用服务原生工具,或者提供一个能够验证关键依赖的轻量级健康接口。健康检查应尽量快速、无副作用,并返回明确的退出状态。
- interval:两次检查之间的间隔。
- timeout:单次检查允许执行的最长时间。
- retries:连续失败多少次后标记为不健康。
- start_period:为慢启动服务预留初始化时间。
如果 start_period 太短,服务可能在正常初始化期间被频繁判定失败;如果重试周期过长,又会导致问题暴露缓慢。应结合真实启动日志调整,而不是机械复制示例参数。
四、启动顺序异常的排查步骤
- 检查解析结果:执行 docker compose config,确认缩进、变量替换和合并后的配置是否符合预期。
- 查看服务状态:执行 docker compose ps,观察依赖容器是否处于 running、healthy 或 unhealthy 状态。
- 跟踪启动日志:执行 docker compose logs -f db app,重点比较数据库就绪时间与应用首次连接时间。
- 查看健康详情:执行 docker inspect 容器名,在 State.Health 中检查每次测试的输出、退出码和持续时间。
- 进入容器手动测试:使用 docker compose exec db 执行与 healthcheck 相同的命令,确认工具、变量、用户权限及目标地址均正确。
- 重建而非只重启:修改 Compose 配置后,可执行 docker compose up -d --force-recreate,避免旧容器配置造成判断偏差。
五、仍然异常时重点检查这些细节
1. 不要把 localhost 当成其他容器
容器内的 localhost 指向当前容器自身。app 访问数据库时,应使用 Compose 服务名,例如 db:5432,而不是 localhost:5432。服务之间通常通过 Compose 网络和服务名通信,不需要依赖宿主机映射端口。
2. 注意 restart 的实际含义
服务级 restart 策略主要控制容器退出后的重启行为,并不能替代健康检查。依赖项在运行期间变为 unhealthy,也不代表所有依赖它的容器都会自动按照完整依赖链重启。因此,应用自身仍应具备连接重试、指数退避和连接恢复能力。🛠️
3. 检查是否只启动了单个服务
如果使用特殊参数跳过依赖,或者只对某个服务执行启动、运行命令,依赖链可能不会按预期建立。排查时应记录实际执行的完整命令,并确认项目目录、Compose 文件以及环境变量文件没有选错。
4. 排除循环依赖
A 等待 B 健康,B 又等待 A 启动,会形成无法合理满足的依赖关系。应重新划分服务职责,把数据库迁移、数据初始化等步骤拆成独立的一次性服务,形成“数据库就绪 → 初始化成功 → 应用启动”的单向链路。
六、更可靠的工程实践
depends_on 适合管理 Compose 启动阶段,但不应成为系统可用性的唯一保障。生产环境中,数据库可能在应用运行数小时后短暂重启,网络也可能发生抖动,此时启动顺序已经无法提供帮助。应用层必须能够处理临时连接失败,并在依赖恢复后重新建立连接。
建议同时落实三层保护:Compose 使用 service_healthy 控制首次启动;依赖服务提供准确的健康检查;应用实现有限重试与故障恢复。对于迁移任务,则使用一次性容器并通过 service_completed_successfully 阻止迁移失败后继续启动主应用。
总结
排查 Docker Compose 启动顺序异常时,首先要确认问题究竟是“容器没有按依赖顺序创建”,还是“容器已运行但内部服务尚未就绪”。短格式 depends_on 只能满足基础顺序需求,真正需要等待可用状态时,应结合 healthcheck 与 condition: service_healthy。最后再通过 config、ps、logs 和 inspect 逐层验证,通常就能定位到配置解析、检查命令、网络地址或应用重试机制上的问题。🚀