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-alpinepostgres:15.4-alpineredis:7.2.3-alpineopenjdk:17-jdk-alpine(构建Spring Boot镜像用)
我建议你至少用Compose V2,因为V2支持depends_on中的condition: service_healthy,这是实现启动顺序控制的关键特性。V1不支持,只能写shell脚本轮询。
3. 方案设计:网络、卷、健康检查三位一体
我的设计思路分三层:
- 网络隔离与互通:所有服务放在同一个自定义网络
app_net内,服务间通过服务名访问。不映射数据库端口到宿主机,只暴露Nginx的80端口,减少攻击面。 - 持久化与数据安全:PostgreSQL数据目录挂载到宿主机
./data/postgres,Redis的AOF文件挂载到./data/redis。容器删了数据不丢,这是生产环境的底线。 - 健康检查 + 条件依赖:给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
这配置文件里有个细节:nginx的depends_on是service_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条件依赖,本质是把“我要先启动谁”的顺序逻辑,转化为“谁先就绪”的状态逻辑。这是声明式编排和命令式脚本的核心区别。从这次实践中我总结三条经验:
- 所有有依赖关系的服务,必须配置健康检查。没有健康检查的
depends_on只是摆设。 - 数据库和缓存这类有状态服务,挂载卷是必须的。容器重建后数据丢失,这在生产环境是不可接受的。
- 版本要新,但不是最新。Compose V2.20+才支持
condition: service_healthy,别用CentOS默认的老版本。
最后留个问题给你:如果你的Spring Boot应用连接PostgreSQL时,数据库虽然健康但还没有创建好业务表(比如需要执行迁移脚本),你的健康检查应该怎么做?欢迎在评论区讨论。