一、问题背景:docker run的失控时刻
上个月我负责的一个电商后台项目终于要上测试环境了。按照惯例,我准备了一堆docker run命令——Nginx一个、后端两个实例、PostgreSQL一个、Redis一个、RabbitMQ一个。结果第一天就翻车了:后端容器启动时数据库还没就绪,连接池初始化直接抛异常,Spring Boot启动失败后容器退出,Docker重启策略又拉起来,但数据库这时候刚好在初始化……循环了七八次,日志刷屏,最后我不得不手动清掉所有容器重新来。
这还不是最痛苦的。每次改配置、换端口、调卷路径,都要在一堆历史命令里找半天。更别提测试环境要多副本,我得复制粘贴改端口——这根本不是人干的活。
二、环境与版本:别再踩Compose v2的坑
先交代一下我的环境,避免版本不一致导致配置不生效:
- Docker Engine:24.0.7(Linux内核 5.15.0)
- Docker Compose:v2.24.2(注意,v1的
version:字段已废弃,不要再写) - 宿主机:Ubuntu 22.04 LTS,8核16G
- 镜像版本:nginx:1.25-alpine、postgres:16.1-alpine、redis:7.2-alpine、rabbitmq:3.12-management-alpine、openjdk:17-jdk-slim(后端镜像基于此构建)
这里强调一下,Compose v2默认就是docker compose(中间有空格),不再是docker-compose。另外,depends_on的condition字段在v2中终于支持service_healthy了,这是解决启动顺序的关键。
三、方案设计:三层网络 + 显式健康检查
我的设计思路分三部分:
1. 自定义网络隔离
默认的bridge网络所有容器互通,但生产环境我习惯分三层:
- frontend_net:只有Nginx暴露80端口,承接外部流量
- backend_net:后端服务与中间件通信
- data_net:数据库、Redis、RabbitMQ之间隔离,避免前端容器直接碰数据库
2. 卷挂载策略
- PostgreSQL数据目录挂到宿主机/data/postgres——容器删了数据还在,这不用多说
- 后端日志目录挂载./logs/app:/logs,方便开发直接看日志文件
- Nginx静态资源挂载./web:/usr/share/nginx/html,前端发版直接替换文件
3. 启动顺序控制
核心逻辑:depends_on + condition: service_healthy。每个中间件必须通过健康检查,后端容器才启动;Nginx最后启动,确保后端真正ready后才接入流量。
四、核心实现:docker-compose.yml全解
直接上配置,这是我家底:
version: "3.9" # 兼容性声明,v2其实会忽略,但保留无妨
networks:
frontend_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24
backend_net:
driver: bridge
ipam:
config:
- subnet: 172.29.0.0/24
data_net:
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24
volumes:
postgres_data:
driver: local
rabbitmq_data:
driver: local
services:
postgres:
image: postgres:16.1-alpine
container_name: ecommerce-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ecommerce
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${DB_PASSWORD} # 从.env读取
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-sql:/docker-entrypoint-initdb.d:ro # 首次初始化脚本
networks:
- data_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d ecommerce"]
interval: 5s
timeout: 3s
retries: 12
start_period: 10s
redis:
image: redis:7.2-alpine
container_name: ecommerce-redis
restart: unless-stopped
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
volumes:
- ./redis-data:/data
networks:
- data_net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
rabbitmq:
image: rabbitmq:3.12-management-alpine
container_name: ecommerce-rabbitmq
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- data_net
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 6
start_period: 20s # RabbitMQ启动慢,给足时间
backend:
build: ./backend
image: ecommerce-backend:1.0
container_name: ecommerce-backend-1
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: docker
DB_HOST: postgres
DB_PORT: 5432
REDIS_HOST: redis
REDIS_PORT: 6379
RABBIT_HOST: rabbitmq
RABBIT_PORT: 5672
volumes:
- ./logs/app:/logs
networks:
- backend_net
- data_net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
nginx:
image: nginx:1.25-alpine
container_name: ecommerce-nginx
restart: unless-stopped
depends_on:
backend:
condition: service_healthy
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./web:/usr/share/nginx/html:ro
networks:
- frontend_net
- backend_net
注意几个细节:
start_period很关键——PostgreSQL首次初始化可能要15-20秒,如果设置太短,健康检查会在初始化期间误报失败,导致后端等不到就绪就启动。- 后端健康检查用的是Spring Boot Actuator的
/actuator/health,需要在项目里加上依赖。 - 密码从
.env文件读取,不写死在yml里。.env格式:DB_PASSWORD=yourpass,并确保.gitignore忽略它。
五、踩坑与优化:从90秒到45秒
坑1:depends_on不加condition等于白写
Compose v2默认的depends_on只是启动顺序,不等待服务就绪。我第一次写的时候只写了depends_on: [postgres, redis, rabbitmq],结果后端照样连不上数据库。必须显式加condition: service_healthy。
坑2:RabbitMQ健康检查超时设置太短
RabbitMQ容器起来后,管理插件初始化需要时间。如果start_period给10秒,retries给3次,每次间隔5秒,总共25秒——在低配机器上大概率失败。我调到start_period: 20s,retries: 6,终于稳定。
坑3:Nginx反代后端,连接超时
Nginx默认proxy_read_timeout 60s,但后端有接口要跑80秒(导出Excel报表),直接504。在conf里加:
location /api/ {
proxy_pass http://backend:8080;
proxy_connect_timeout 5s;
proxy_read_timeout 120s;
}
优化:合并健康检查,缩短冷启动
之前的配置里,PostgreSQL和Redis健康检查间隔都是10秒,最坏情况要等10秒才探测一次。我把间隔统一改为5秒,start_period精确到服务实际初始化时间,通过docker compose logs -f观察日志,反复调整后整体冷启动从90秒缩短到45秒。
做个小对比:
| 项目 | 优化前 | 优化后 |
|---|---|---|
| PostgreSQL就绪 | 22s | 15s(start_period精准) |
| 后端启动 | 35s(含等待) | 20s(依赖条件触发) |
| 总耗时 | 90s+ | 45s |
| 容器重启次数 | 3-5次 | 0次 |
六、效果数据与总结
这套配置在测试环境跑了三周,20+个功能模块迭代,容器零意外重启。最直观的变化:
- 部署时间:从手动敲15条命令约10分钟,变成
docker compose up -d一条命令2分钟搞定 - 故障恢复:宿主机重启后,
restart: unless-stopped自动拉起所有服务,健康检查失败自动重启,无需人工干预 - 日志排查:
docker compose logs -f backend --tail=100直接看后端日志,不用再docker logs -f 容器ID了
最后给个建议:别在Compose文件里写死版本号以外的配置,环境差异用.env管理;健康检查的start_period一定要根据实际服务启动时间调,宁长勿短;网络划分别图省事全塞一个brige里,隔离性差不说,排查问题也不方便。这套配置我放到了公司GitLab的CI里,每次代码合并自动构建镜像并docker compose up -d,测试同学再也没因为环境问题找我聊过天。