1. 问题背景:不是编排工具不行,是编排姿势不对

上个月接手了一个中台服务,架构不复杂:一个Spring Boot应用(gateway),依赖PostgreSQL存元数据、Redis做缓存,前面挂着Nginx做负载。按老习惯,我写了一个Docker Compose文件,把四个服务塞进去,docker-compose up -d一敲,等着看日志。

结果?Spring Boot启动比数据库快,等它去连PostgreSQL时报Connection refused,整个应用直接退出。我加上restart: always,但它重试了三次还是失败,因为PostgreSQL初始化需要时间,而Spring Boot的重试间隔远小于数据库的启动时间。

这不是Docker Compose的锅,是我没理解它的编排模型:depends_on只保证启动顺序,并不保证服务可用。如果你也遇到过类似问题,继续往下看,这篇博客会给你一个可落地的解决方案。

2. 环境与版本:老版本真的会踩坑

先交代我的环境,免得你版本不同走弯路:

  • Docker Engine:24.0.7
  • Docker Compose:v2.24.2(注意,这里用的是docker compose命令,不是docker-compose,后者已经进入维护模式)
  • 基础镜像:
  • nginx:1.25.3-alpine
  • postgres:15.4-alpine
  • redis:7.2.3-alpine
  • openjdk:17-jdk-alpine(构建Spring Boot镜像用)

我建议你至少用Compose V2,因为V2支持depends_on中的condition: service_healthy,这是实现启动顺序控制的关键特性。V1不支持,只能写shell脚本轮询。

3. 方案设计:网络、卷、健康检查三位一体

我的设计思路分三层:

  1. 网络隔离与互通:所有服务放在同一个自定义网络app_net内,服务间通过服务名访问。不映射数据库端口到宿主机,只暴露Nginx的80端口,减少攻击面。
  2. 持久化与数据安全:PostgreSQL数据目录挂载到宿主机./data/postgres,Redis的AOF文件挂载到./data/redis。容器删了数据不丢,这是生产环境的底线。
  3. 健康检查 + 条件依赖:给PostgreSQL和Redis配置healthcheck,Spring Boot的depends_on里用condition: service_healthy。Nginx依赖Spring Boot健康后才能启动,避免上游不可达时的502。

4. 核心实现:docker-compose.yml完整配置

下面是完整配置文件,我加了详细注释,你可以直接复用。注意几个细节:start_period给数据库初始化留了10秒缓冲,interval设置成5秒,避免频繁探活。

version: "3.8"

networks:
  app_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

services:
  # ---------- 数据库 ----------
  postgres:
    image: postgres:15.4-alpine
    container_name: app-postgres
    restart: always
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: app_pass_2024
      POSTGRES_DB: app_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

  # ---------- 缓存 ----------
  redis:
    image: redis:7.2.3-alpine
    container_name: app-redis
    restart: always
    command: ["redis-server", "--appendonly", "yes", "--requirepass", "redis_pass_2024"]
    volumes:
      - redis_data:/data
    networks:
      - app_net
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redis_pass_2024", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  # ---------- Spring Boot 应用 ----------
  gateway:
    build:
      context: ./gateway
      dockerfile: Dockerfile
    image: app-gateway:1.0.0
    container_name: app-gateway
    restart: always
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/app_db
      SPRING_DATASOURCE_USERNAME: app_user
      SPRING_DATASOURCE_PASSWORD: app_pass_2024
      SPRING_DATA_REDIS_HOST: redis
      SPRING_DATA_REDIS_PASSWORD: redis_pass_2024
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app_net

  # ---------- 反向代理 ----------
  nginx:
    image: nginx:1.25.3-alpine
    container_name: app-nginx
    restart: always
    ports:
      - "8080:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    depends_on:
      gateway:
        condition: service_started
    networks:
      - app_net

这配置文件里有个细节:nginxdepends_onservice_started,不是service_healthy。为什么?因为Spring Boot的/actuator/health接口我没在这个示例里加,如果加了,你也可以让它走健康检查。这里我选择让Nginx先启动,因为Nginx对上游不通有容错机制(返回502),不影响自身启动。

5. 踩坑与优化:三个坑,每个都让我折腾过

坑一:pg_isready 认证警告

第一次写healthcheck,我用了pg_isready -U app_user,但没指定数据库名。PostgreSQL会提示warning: extra command-line argument ignored,虽然不影响退出码,但日志很脏。后来改成pg_isready -U app_user -d app_db,干净了。

坑二:Redis密码导致healthcheck失败

redis-cli ping在无密码环境直接返回PONG,加了--requirepass后,不带-a参数会返回NOAUTH Authentication required,退出码非0,容器一直显示unhealthy。我一开始没加-a参数,健康检查一直失败,Spring Boot等不到Redis就绪,循环重启。这个坑卡了我半小时,排查方式是用docker inspect看健康日志。

坑三:镜像构建时间导致depends_on失效

gateway服务用了build指令,如果本地没有缓存,构建可能要2-3分钟。但Compose的depends_on是在服务启动阶段判断的,不是构建阶段。也就是说,如果PostgreSQL先就绪了,但gateway还在构建,它不会等构建完成再启动。解决方式:先把镜像构建好,或者用docker compose up --build -d时注意观察日志。

优化点:启动顺序的最终验证

我用一个脚本做最终验证:

#!/bin/bash
# check_startup.sh
echo "等待数据库健康..."
until [ "$(docker inspect -f '{{.State.Health.Status}}' app-postgres)" == "healthy" ]; do
  sleep 2
done
echo "数据库已就绪,开始启动网关..."
docker compose up -d gateway
until [ "$(docker inspect -f '{{.State.Health.Status}}' app-gateway)" == "healthy" ]; do
  sleep 3
done
echo "网关已就绪,启动Nginx..."
docker compose up -d nginx

这个脚本不是必须的,因为depends_on已经帮你做了。但在调试阶段,它能帮你定位是哪一层卡住了。

6. 效果数据:从70%成功率到100%

改造前(无健康检查,仅depends_on短格式):

指标 数值
启动成功率(10次尝试) 7/10
平均就绪时间 约85秒
失败原因 Spring Boot连不上PostgreSQL,重试3次后退出

改造后(健康检查 + 条件依赖):

指标 数值
启动成功率(10次尝试) 10/10
平均就绪时间 约50秒
日志中的异常次数 0

成功率从70%提升到100%,平均就绪时间缩短了约40%。主要收益是省去了无意义的重启等待,数据库一旦健康,Spring Boot一次连接成功,不再有退避重试的逻辑浪费。

7. 总结:编排的实质是声明期望状态

Docker Compose的depends_on条件依赖,本质是把“我要先启动谁”的顺序逻辑,转化为“谁先就绪”的状态逻辑。这是声明式编排和命令式脚本的核心区别。从这次实践中我总结三条经验:

  1. 所有有依赖关系的服务,必须配置健康检查。没有健康检查的depends_on只是摆设。
  2. 数据库和缓存这类有状态服务,挂载卷是必须的。容器重建后数据丢失,这在生产环境是不可接受的。
  3. 版本要新,但不是最新。Compose V2.20+才支持condition: service_healthy,别用CentOS默认的老版本。

最后留个问题给你:如果你的Spring Boot应用连接PostgreSQL时,数据库虽然健康但还没有创建好业务表(比如需要执行迁移脚本),你的健康检查应该怎么做?欢迎在评论区讨论。