切换语言
切换主题

Docker Compose服务依赖:健康检查配置解决数据库启动顺序问题

Docker Compose服务依赖和健康检查配置示意图

周五晚上十点,我盯着终端里滚动的错误日志,心里盘算着今晚能不能按时下班。

web_1  | Error: connect ECONNREFUSED 127.0.0.1:5432
web_1  | at TCPConnectWrap.afterConnect
db_1   | PostgreSQL init process complete; ready for start up.
web_1  | Exited with code 1
web_1  | Restarting...

应用容器疯狂重启,数据库倒是启动了,但总是慢一拍。我检查docker-compose.yml——depends_on配了啊,为什么还不行?

其实这个问题困扰过太多开发者。你可能也遇到过:本地开发环境docker-compose up,前两次启动必定失败,要等个十几秒重启几次才能正常跑起来。团队里新来的同事问你”这正常吗”,你也只能尴尬地说”多试几次就好了”。

问题的根源很简单:Docker的depends_on只管容器启动顺序,不管服务是否真的准备好了

这篇文章我会手把手教你:

  • depends_on的三种condition配置(90%的人只知道默认的)
  • PostgreSQL和MySQL的正确healthcheck写法(带完整配置)
  • 告别wait-for-it脚本的现代化方案
  • 一套故障排查清单,让你的容器启动稳如老狗

为什么depends_on不够用:启动≠就绪

Docker官方文档里有一句话特别重要,但很容易被忽略:

Compose does not wait until a container is “ready”, only until it’s running.

翻译过来就是:Compose只等容器跑起来,不管服务是不是真的能用。

容器启动和服务就绪的时间差

想象一下PostgreSQL容器的启动过程:

  1. 0秒:Docker拉起容器,postgres进程启动 ← depends_on在这里就放行了
  2. 2秒:初始化数据目录
  3. 5秒:加载配置文件
  4. 8秒:执行init脚本(如果有的话)
  5. 12秒:终于ready,可以接受连接了

这中间有12秒的gap。如果你的Web应用在第1秒就尝试连接数据库,结果必然是”Connection refused”。

真实案例更夸张。我之前维护一个遗留项目,数据库初始化脚本要导入500MB的测试数据,光这个过程就要跑40秒。用默认的depends_on配置,应用容器至少要crash重启5次才能连上。

depends_on的三种condition

很多人不知道,depends_on其实支持三种condition:

services:
  web:
    depends_on:
      db:
        condition: service_started  # 默认,容器启动就OK
        # condition: service_healthy  # 等待健康检查通过
        # condition: service_completed_successfully  # 等待容器成功退出(适合init容器)

service_started(默认):只要容器在running状态就继续。这就是为什么配了depends_on还是会出问题。

service_healthy:必须等healthcheck通过,容器状态变成”healthy”才继续。这才是我们真正需要的。

service_completed_successfully:等待容器成功退出(exit code 0)。适合数据迁移这种一次性任务。

为什么默认不是service_healthy?

你可能会问,既然service_healthy这么好用,为什么不设为默认?

两个原因:

  1. 不是所有服务都需要健康检查(比如纯stateless的worker)
  2. 健康检查需要你自己配置,Docker不知道你的服务怎么算”准备好了”

这就引出了下一个话题:如何配置健康检查。

健康检查(healthcheck)完全配置指南

healthcheck的原理很直白:Docker定期跑一个命令,如果返回0就算健康,返回1就算不健康。

完整的healthcheck配置

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]  # 检查命令
      interval: 10s       # 每10秒检查一次
      timeout: 5s         # 单次检查5秒超时
      retries: 3          # 失败3次才标记为unhealthy
      start_period: 30s   # 启动后30秒内的失败不计入retries

这五个参数都很重要,咱们一个个说。

test:检查命令

格式有两种:

# 方式1:使用shell(推荐)
test: ["CMD-SHELL", "pg_isready -U postgres"]

# 方式2:直接执行命令(不经过shell)
test: ["CMD", "pg_isready", "-U", "postgres"]

大部分情况用CMD-SHELL就行,因为可以用管道、重定向这些shell特性。

常见坑:检查命令里的工具必须存在于镜像中。比如用curl检查HTTP接口,但镜像里没装curl,那healthcheck永远失败。我之前就踩过这个坑,花了半小时才发现要在Dockerfile里加一行RUN apk add curl

interval:检查间隔

多久检查一次。太频繁浪费资源,太慢响应迟钝。

  • 10秒是个不错的默认值,适合大多数场景
  • 数据库这种关键服务可以设5秒
  • 轻量级服务15-30秒也行

timeout:单次超时

单次检查的超时时间。如果命令卡住了,Docker会等timeout这么久再放弃。

设太短容易误报,设太长影响故障检测速度。5-10秒是个安全范围。

retries:失败重试次数

连续失败多少次才标记为unhealthy。

这个是防抖机制。网络偶尔抖动、数据库短暂重载都可能导致单次检查失败,retries让系统更健壮。

3-5次比较合理。retries=1太敏感,retries=10又太迟钝。

start_period:启动宽限期(最容易忽略的)

这是最容易被忽略、也最容易踩坑的参数。

start_period内的失败不会计入retries。换句话说,给服务一个”启动缓冲期”。

为什么重要?数据库启动需要时间。PostgreSQL要初始化数据目录、MySQL要加载表索引。如果没有start_period,健康检查在第2秒就开始失败计数,可能retries还没用完服务就被标记为unhealthy了。

推荐值

  • PostgreSQL/MySQL:30-60秒
  • 轻量级服务(Redis):15-30秒
  • 有大量初始化脚本的:可以设到120秒

我一般设60秒,宁可多等一会儿,也不要误报。

常见错误和避坑指南

错误1:环境变量引用不正确

# ❌ 错误:Compose会在启动前插值,容器里拿到的是宿主机的变量值
test: ["CMD", "mysqladmin", "ping", "-p$MYSQL_ROOT_PASSWORD"]

# ✅ 正确:用$$转义,让容器内的shell解析
test: ["CMD-SHELL", "mysqladmin ping -p$$MYSQL_ROOT_PASSWORD"]

错误2:start_period设太短

# ❌ 数据库还没初始化完就开始计数,很快被标记为unhealthy
healthcheck:
  test: ["CMD", "pg_isready"]
  interval: 5s
  retries: 3
  start_period: 10s  # 太短了!

# ✅ 给足够的启动时间
healthcheck:
  start_period: 60s  # 安心多了

错误3:检查工具不存在

这个错误特别隐蔽,因为Docker只会静默失败。

# ❌ 如果镜像里没有curl,healthcheck永远失败
test: ["CMD", "curl", "-f", "http://localhost/health"]

# ✅ 确保工具存在,或者用镜像自带的工具
test: ["CMD", "wget", "--spider", "http://localhost/health"]  # Alpine镜像自带wget

健康检查配好了,接下来看具体数据库怎么配。

PostgreSQL健康检查实战配置

PostgreSQL官方镜像自带一个神器:pg_isready

这个工具专门用来检查PostgreSQL是否ready,比自己写SQL查询靠谱多了。

基础配置(推荐)

version: '3.8'

services:
  web:
    image: node:20-alpine
    depends_on:
      db:
        condition: service_healthy  # 关键:等待健康检查通过
        restart: true  # 数据库重启时,应用也跟着重启
    environment:
      DATABASE_URL: postgresql://postgres:password@db:5432/myapp
    command: npm start

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

pg_isready命令详解

pg_isready -U postgres -d myapp
  • -U:指定用户名,必须是实际存在的用户
  • -d:指定数据库名(可选,但建议加上)

为什么要加-U?如果不指定,pg_isready会尝试用当前系统用户连接,日志里会有一堆warning。虽然不影响功能,但看着烦。

进阶配置:加实际查询

pg_isready只检查端口是否可达,不保证数据库真的能执行查询。如果你需要更严格的检查:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres && psql -U postgres -d myapp -c 'SELECT 1'"]
  interval: 10s
  timeout: 10s  # 注意timeout要加大,因为有额外的查询
  retries: 3
  start_period: 60s

SELECT 1是最简单的查询,如果执行成功说明数据库不仅启动了,而且能正常处理SQL。

不过大部分场景下,基础的pg_isready就够了。

实际运行效果

配置好后,启动看看:

$ docker-compose up

Creating network "myapp_default" ... done
Creating myapp_db_1 ... done
Waiting for myapp_db_1 to be healthy... 注意这行
Creating myapp_web_1 ... done

db_1   | PostgreSQL init process complete; ready for start up.
db_1   | database system is ready to accept connections
web_1  | Server listening on port 3000 应用在数据库ready后才启动

你会看到一个明显的停顿——Docker在等db容器变成healthy。这个过程可能要30-60秒,但换来的是零失败启动。

故障排查:容器一直是unhealthy

如果数据库容器一直显示unhealthy,查看健康检查日志:

# 查看容器健康状态
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq

{
  "Status": "unhealthy",
  "FailingStreak": 5,
  "Log": [
    {
      "Start": "2024-12-17T03:15:30Z",
      "End": "2024-12-17T03:15:30Z",
      "ExitCode": 1,
      "Output": "pg_isready: could not connect to server: Connection refused"
    }
  ]
}

常见原因:

  1. start_period太短:数据库还在初始化就开始计数失败
  2. 用户名或数据库名错误:pg_isready连不上
  3. PostgreSQL启动失败:看容器日志docker logs myapp_db_1

MySQL健康检查实战配置

MySQL的健康检查用mysqladmin ping,这是MySQL自带的管理工具。

基础配置(推荐)

version: '3.8'

services:
  web:
    image: node:20-alpine
    depends_on:
      db:
        condition: service_healthy
        restart: true
    environment:
      DATABASE_URL: mysql://root:password@db:3306/myapp
    command: npm start

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: myapp
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

mysqladmin ping命令详解

mysqladmin ping -h localhost -u root -ppassword
  • -h:主机地址(容器内用localhost)
  • -u:用户名
  • -p:密码(注意-p和密码之间没有空格)

如果MySQL正常,这个命令会返回:

mysqld is alive

退出码是0,健康检查通过。

密码的正确处理方式

方式1:直接写密码(适合开发环境)

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]

简单粗暴,但密码硬编码在配置里。

方式2:用环境变量(推荐)

db:
  environment:
    MYSQL_ROOT_PASSWORD: password
  healthcheck:
    test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
    # 注意:用$$ 而不是 $

这里有个坑:必须用$$而不是$

为什么?因为Docker Compose会在启动前解析环境变量。如果用$MYSQL_ROOT_PASSWORD,Compose会在你的宿主机上查找这个变量,而不是容器里的。用$$告诉Compose”别管它,让容器内的shell去解析”。

方式3:无密码检查(最简单,但有争议)

healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]

有些MySQL配置允许本地无密码连接,这种情况下最简单。不过生产环境不推荐。

MySQL 8.0的特殊注意事项

MySQL 8.0默认使用caching_sha2_password认证插件,可能导致一些老客户端连不上。如果你的应用报认证错误,可以强制使用旧的认证方式:

db:
  image: mysql:8.0
  command: --default-authentication-plugin=mysql_native_password
  environment:
    MYSQL_ROOT_PASSWORD: password
    MYSQL_DATABASE: myapp
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 60s

常见问题

问题1:Access denied for user ‘root’@‘localhost’

密码不对,或者环境变量没生效。检查:

  1. MYSQL_ROOT_PASSWORD拼写是否正确
  2. healthcheck里的密码是否匹配
  3. 是否用了$$转义

问题2:容器启动很慢,一直是starting状态

MySQL初始化数据目录需要时间,特别是第一次启动。确保start_period设够大,60秒通常够用。如果有init脚本要导入大量数据,可能要120秒甚至更多。

问题3:健康检查通过了,但应用连不上数据库

可能是网络问题或应用配置问题。检查:

  1. 应用的连接字符串是否正确(主机名用db而不是localhost
  2. Docker网络配置是否正常
  3. docker network inspect查看容器是否在同一网络

其他数据库和服务的健康检查

掌握了PostgreSQL和MySQL,其他服务就触类旁通了。这里给个快速参考表。

Redis

redis:
  image: redis:7-alpine
  healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 15s

redis-cli ping会返回PONG,退出码0。Redis启动很快,start_period可以设短一点。

MongoDB

mongo:
  image: mongo:7
  healthcheck:
    test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 30s

注意:MongoDB 6.0之后用mongosh替代了老的mongo命令。如果用旧版本:

test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]

RabbitMQ

rabbitmq:
  image: rabbitmq:3-management-alpine
  healthcheck:
    test: ["CMD", "rabbitmq-diagnostics", "ping"]
    interval: 10s
    timeout: 5s
    retries: 3
    start_period: 40s

RabbitMQ启动比较慢,start_period建议40秒以上。

通用HTTP服务

如果服务提供HTTP健康检查接口(如/health/ping),可以用wget或curl:

api:
  image: myapp:latest
  healthcheck:
    test: ["CMD", "wget", "--spider", "--quiet", "http://localhost:8080/health"]
    # 或者用curl(如果镜像里有)
    # test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 20s

--spider让wget只检查不下载,--quiet不输出日志。

注意:确保镜像里有wget或curl。Alpine镜像默认有wget,Debian/Ubuntu镜像有curl。

没有专用工具的服务

如果服务没有健康检查工具,可以用netcat (nc)检查端口:

service:
  image: some-service:latest
  healthcheck:
    test: ["CMD-SHELL", "nc -z localhost 9000 || exit 1"]
    interval: 10s
    timeout: 3s
    retries: 3
    start_period: 30s

nc -z只检查端口是否打开,不建立实际连接。不过这只能检查端口,不能确认服务真的ready,比前面的方法粗糙一些。

wait-for-it脚本:现在还需要吗?

如果你搜Docker启动顺序,可能会看到很多文章推荐wait-for-it或wait-for脚本。

这些脚本的思路是:在应用容器的entrypoint里加一段等待逻辑,检查依赖服务的TCP端口是否可达。

传统wait-for-it方案

web:
  image: node:20-alpine
  depends_on:
    - db  # 只是普通depends_on,不检查健康状态
  volumes:
    - ./wait-for-it.sh:/wait-for-it.sh  # 挂载脚本
  command: ["/wait-for-it.sh", "db:5432", "--", "npm", "start"]

wait-for-it.sh会循环检查db:5432端口,直到可连接才执行npm start

为什么不推荐了?

2024年的最佳实践是:能用原生healthcheck就别用脚本

原因:

  1. 配置更清晰:健康检查逻辑在数据库服务里,一眼就看出依赖关系
  2. 无需额外文件:不用维护脚本、不用挂载volume
  3. 功能更强:healthcheck能检查实际服务就绪度,TCP端口检查太粗糙
  4. 复用性更好:healthcheck配一次,所有依赖它的服务自动受益

社区里有篇很火的文章就叫”Forget wait-for-it, use docker-compose healthcheck and depends_on instead”。

什么时候还需要wait-for-it?

有两种特殊情况:

情况1:无法修改镜像或compose配置

比如用第三方镜像,镜像本身没有healthcheck,你又没权限加。这时候在应用侧加wait-for-it是唯一选择。

情况2:需要等待多个服务

./wait-for-it.sh db:5432 redis:6379 rabbitmq:5672 -- npm start

虽然depends_on也能配多个,但wait-for-it写起来更简洁。不过这种场景不多见。

其他替代工具

除了wait-for-it,还有几个类似工具:

  • dockerize:Go写的,功能更丰富,支持环境变量模板
  • wait-for:wait-for-it的简化版,纯shell实现
  • docker-compose-wait:Python写的,支持HTTP检查

但说实话,2024年了,能用healthcheck就别折腾这些了。

故障排查清单和最佳实践

配置好了还是不行?按这个清单排查,90%的问题都能解决。

快速诊断命令

# 1. 查看所有容器状态
$ docker-compose ps

NAME       COMMAND    SERVICE   STATUS              PORTS
myapp_db   postgres   db        healthy             5432/tcp
myapp_web  npm start  web       running             0.0.0.0:3000->3000/tcp

# 2. 查看健康检查详情
$ docker inspect --format='{{json .State.Health}}' myapp_db_1 | jq

# 3. 查看容器日志
$ docker-compose logs db
$ docker-compose logs web

# 4. 实时跟踪日志
$ docker-compose logs -f --tail=100

常见问题决策树

问题:容器一直显示starting,从不变成healthy

  1. 检查start_period是否太短 → 改成60s试试
  2. 查看健康检查命令是否正确 → docker inspect看实际执行的命令
  3. 进入容器手动执行健康检查命令 → docker exec -it myapp_db_1 pg_isready -U postgres

问题:容器变成unhealthy后又恢复healthy,反复横跳

  1. interval设太短,资源不足 → 改成10s或15s
  2. retries太少,偶发故障就触发 → 改成5
  3. 数据库确实有性能问题 → 查看数据库日志

问题:健康检查通过,但应用还是连不上数据库

  1. 应用的连接串是否正确 → 主机名用service名(如db),不是localhost
  2. 端口映射是否正确 → 容器间通信用内部端口(5432),不是映射后的端口
  3. 网络配置是否正确 → 确认所有服务在同一network

生产环境最佳实践

1. 参数推荐值(保守配置)

healthcheck:
  interval: 10s          # 平衡响应速度和资源消耗
  timeout: 5s            # 给命令足够时间执行
  retries: 5             # 容忍偶发故障
  start_period: 60s      # 给数据库充足启动时间

这套参数在大部分场景下都很稳定。如果你的数据库有大量初始化脚本,start_period可以设到120s。

2. 使用restart策略

web:
  depends_on:
    db:
      condition: service_healthy
      restart: true  # 数据库重启时,应用也重启
  restart: unless-stopped  # 容器退出后自动重启

restart: true确保数据库升级或重启后,依赖它的服务也会重启重新连接。

3. 资源限制

健康检查也消耗资源,虽然很少。如果你的系统资源紧张:

healthcheck:
  interval: 30s  # 拉长检查间隔
  timeout: 3s    # 缩短超时

不过说实话,healthcheck的开销通常可以忽略,除非你跑了几百个容器。

4. 监控健康状态

生产环境建议用监控工具跟踪健康检查状态。Docker的健康检查事件可以被Prometheus、Grafana等工具采集。

# docker-compose.yml
services:
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    labels:
      - "prometheus.io/scrape=true"  # 让Prometheus采集健康状态

5. 多环境配置

开发环境和生产环境的配置可以不一样:

# docker-compose.yml(开发)
db:
  healthcheck:
    start_period: 30s  # 开发环境数据少,启动快

# docker-compose.prod.yml(生产)
db:
  healthcheck:
    start_period: 120s  # 生产环境数据多,启动慢
    interval: 5s        # 更频繁的检查

使用时指定配置文件:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

调试技巧

技巧1:手动测试健康检查命令

进入容器,手动跑健康检查命令,看看到底哪里出错:

$ docker exec -it myapp_db_1 sh
/# pg_isready -U postgres -d myapp
/var/run/postgresql:5432 - accepting connections
/# echo $?
0 返回0说明成功

技巧2:临时禁用健康检查

调试时可以先把healthcheck注释掉,用普通的depends_on,排除健康检查本身的问题:

web:
  depends_on:
    - db  # 临时用简单模式
    # db:
    #   condition: service_healthy

确认应用能正常连数据库后,再加回healthcheck。

技巧3:查看Docker事件日志

Docker会记录所有容器事件,包括健康检查状态变化:

$ docker events --filter 'event=health_status'

2024-12-17T03:15:30.123456789Z container health_status: healthy (name=myapp_db_1)
2024-12-17T03:16:45.987654321Z container health_status: unhealthy (name=myapp_db_1)

这能帮你找到容器什么时候变unhealthy的,结合日志时间戳定位问题。

结论

回到文章开头的问题:为什么配了depends_on还是不行?

答案就三个字:不够用

depends_on默认只管容器启动,不管服务就绪。数据库容器running了不代表能接受连接,这中间的gap是我们踩坑的根源。

解决方案也很直接:

  1. 给数据库配上healthcheck,用pg_isready或mysqladmin ping检查实际就绪状态
  2. 用service_healthy condition,让应用等待健康检查通过
  3. 设置合理的start_period(60秒起步),给数据库足够的初始化时间

这三板斧下来,容器启动失败的问题基本就消失了。

最后给几个快速行动建议:

  • 立即可做:复制文章里的PostgreSQL或MySQL配置到你的项目,改改环境变量就能用
  • 今晚回家:把团队项目里的depends_on都升级成service_healthy,一劳永逸
  • 下周团队会:分享给同事,统一团队的配置标准

如果你遇到了文章没覆盖到的问题,欢迎在评论区留言。Docker Compose的坑还有很多,咱们一起填。

最后,如果这篇文章帮你解决了困扰已久的启动问题,点个赞让我知道。这种”终于搞定了”的瞬间,是我写这篇文章的动力。

13 分钟阅读 · 发布于: 2025年12月17日 · 修改于: 2025年12月26日

评论

使用 GitHub 账号登录后即可评论

相关文章