Featured image of post Docker 启动依赖等待:wait-for-it 与 healthcheck 实战

Docker 启动依赖等待:wait-for-it 与 healthcheck 实战

容器启动顺序的解法:wait-for-it.sh / docker-compose-wait 等待外部依赖、depends_on 的局限、healthcheck 主动健康检查

docker-compose up 时一个经典坑:app 容器比 mysql 容器起得还快,app 启动时报 “connection refused”depends_on 只控制"启动顺序",不等待依赖服务真的可用——你需要更稳的解法。

这一篇把"启动依赖等待"的所有主流方案收齐:wait-for-it.shdocker-compose-waitdepends_on 的局限healthcheck 主动检查

阅读对象:用 docker-compose 编排多服务、被"启动顺序"问题坑过的开发者 覆盖范围:wait-for-it + docker-compose-wait + healthcheck + depends_on 局限 + 最佳实践

一、问题的本质:依赖启动的"就绪"问题

1
2
3
4
5
6
7
services:
  db:
    image: mysql:8
  app:
    image: myapp:latest
    depends_on:
      - db

depends_on: db真实行为

  • ✅ 控制启动顺序——db 一定先于 app 启动
  • 不等待 db 真的可用——db 容器起来后,MySQL 进程可能还要 5-10 秒才能接受连接

结果app 启动时 mysql:3306 可能还没监听,连接失败。

二、解法 1:wait-for-it.sh

wait-for-it 是社区里最常用的"端口可达性探测"小脚本。

2.1 基础用法

1
2
chmod +x ./wait-for-it.sh
./wait-for-it.sh www.baidu.com:80 -t 30 -- echo "baidu is up"

参数:

  • <host>:<port>——要等待的地址
  • -t <seconds>——超时时间,默认 15s,设为 0 表示无限等
  • -- 后跟"等到了执行的命令"

输出示例:

1
2
3
wait-for-it.sh: waiting 15 seconds for www.baidu.com:80
wait-for-it.sh: www.baidu.com:80 is available after 0 seconds
baidu is up

2.2 在 docker-compose 中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
services:
  my_nacos:
    image: nacos/nacos-server:latest
    container_name: my_nacos

  test-app:
    container_name: test-app
    image: java:8
    restart: always
    environment:
      TZ: Asia/Shanghai
    ports:
      - "9001:9001"
    volumes:
      - ./backend/test-app.jar:/data/test-app.jar
      - ./wait-for-it.sh:/wait-for-it.sh
    # 关键:用 wait-for-it 等 nacos 起来再启动应用
    command: [
      "/wait-for-it.sh", "my_nacos:8848", "--",
      "java", "-jar", "/data/test-app.jar"
    ]
    depends_on:
      - my_nacos

机制

  1. wait-for-it.sh 轮询 my_nacos:8848 是否 TCP 可达
  2. 可达后执行后面的 java -jar 命令
  3. 30 秒超时(默认 15s)后放弃,启动失败

2.3 host 网络模式下的特例

如果 network_mode: "host"容器名解析失效——my_nacos:8848 不通。

1
2
3
4
services:
  app:
    network_mode: "host"
    command: ["/wait-for-it.sh", "localhost:8848", "--", "java", "-jar", "/data/app.jar"]

Why:host 网络下容器共享宿主机网络栈,容器内看到的 localhost 就是宿主机的 localhost——my_nacos 容器名解析不到,得用 localhost

三、解法 2:docker-compose-wait

docker-compose-wait 是用 Python 写的"等待一组服务"工具,比 wait-for-it 强大:

  • ✅ 支持 多个 host:port
  • ✅ 支持 环境变量配置(不用改 compose 文件)
  • ✅ 支持 健康检查命令(不仅是端口)
  • ✅ 支持 自定义等待策略

3.1 安装

在 Dockerfile 里装:

1
2
3
4
RUN pip install docker-compose-wait
# 或
RUN curl -Lo /wait https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait \
  && chmod +x /wait

3.2 compose 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
services:
  db:
    image: mysql:8
    environment:
      WAIT_HOSTS: db:3306

  app:
    build: .
    environment:
      WAIT_HOSTS: db:3306, redis:6379
      WAIT_TIMEOUT: 60
    command: ["/wait", "&&", "java", "-jar", "/data/app.jar"]
    depends_on:
      - db
      - redis

优势

  • 多端口一行配置
  • 超时、间隔都走环境变量
  • 支持 healthcheck 字符串——WAIT_HOSTS: "tcp://db:3306, http://api/health"

四、解法 3:depends_on 长语法(Docker Compose v2)

Docker Compose v2(2020-08 GA)扩展了 depends_on

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  db:
    image: mysql:8
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10

  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy

What

  • condition: service_healthy——等 db 的 healthcheck 通过再启动 app
  • condition: service_started——等 db 容器启动(默认行为
  • 不需要 wait-for-it 这种外部脚本

  • 必须配 healthcheck——不带 healthcheck,service_healthy 永远不通过
  • Docker Compose v1 不支持——必须升 v2(docker compose 插件)

五、解法 4:healthcheck + restart

最朴素但有效的方案:给容器加 healthcheck,启动失败自动重试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  app:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s
    restart: on-failure

机制

  • 容器启动后 30s 内(start_period)不计入健康检查失败次数
  • 之后每 10s 查一次,连续 5 次失败标 unhealthy
  • Docker Compose v2 的 service_healthy 会等这个状态

配合 depends_on 长语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  db:
    image: mysql:8
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10

  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy

这是 Docker Compose v2 时代的最佳实践——不需要外部脚本,配置即文档。

六、解法 5:应用层重试

最不优雅但最 robust 的方案:应用启动时自带重试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Spring Boot 风格
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        // 启动时重试 N 次
        for (int i = 0; i < 10; i++) {
            try {
                SpringApplication.run(App.class, args);
                break;
            } catch (Exception e) {
                Thread.sleep(5000);
            }
        }
    }
}

或用 Spring Boot 的 datasource retry:

1
2
3
4
spring:
  datasource:
    hikari:
      initialization-fail-timeout: 60000  # 60s 内持续重试

优势不依赖任何外部编排——裸跑 docker run 也能 work 劣势:耦合进应用代码、不是纯基础设施问题

七、对比表

方案配置复杂度适用场景推荐度
wait-for-it.sh单端口、简单场景、Compose v1⭐⭐⭐
docker-compose-wait多端口、需要可配置⭐⭐⭐
depends_on + healthcheck (v2)Compose v2 + 多服务依赖⭐⭐⭐⭐⭐
应用层重试框架级稳健性⭐⭐⭐⭐

2020 年后推荐

  1. Compose v2 + depends_on: condition: service_healthy + 每个服务配 healthcheck
  2. 裸 docker run + 应用层重试
  3. wait-for-it / docker-compose-wait 仅在 Compose v1 或老项目使用

八、最佳实践总结

  1. 每个长跑服务都加 healthcheck——不只是依赖方
  2. depends_on 用长语法——condition: service_healthy(Compose v2)
  3. 避免 wait 外部脚本——能配置的不用脚本
  4. 超时一定要设——start_period: 30s 给慢启动服务缓冲
  5. 应用层有兜底——基础设施层等不到时,应用自己重试
  6. restart: on-failure——启动失败时容器自动重试(不是 always,避免无谓重启)

九、要点回顾

  1. depends_on 不等待可用——只控制启动顺序
  2. wait-for-it 适合单端口、Compose v1 项目
  3. docker-compose-wait 适合多端口
  4. healthcheck + service_healthy 是 Compose v2 最佳实践
  5. 应用层重试是最 robust 的兜底
  6. host 网络下容器名解析失效,用 localhost

十、小结

“启动依赖等待"是 docker-compose 里最经典的"看起来能 work 但实际跑起来就崩"的问题。把 healthcheck + depends_on: service_healthy 配成肌肉记忆,比写一堆 wait 脚本清爽

下一步:理解了启动顺序,下一步是 K8s 的 Init Container + Readiness Probe——把"启动依赖等待"从编排层下沉到容器层,配合 Pod lifecycle hook(postStart / preStop)做更精细的启动 / 关停控制。K8s 1.20+ 的 startupProbe 专门解决"慢启动服务"的 readiness 判断问题。

参考资料

使用 Hugo 构建
主题 StackJimmy 设计