1. 问题背景:docker run脚本化部署的痛点
上个月,我把一个电商后台拆成了5个服务:Nginx做反向代理,Spring Boot跑业务逻辑,PostgreSQL存订单,Redis扛缓存,RabbitMQ处理异步消息。刚开始用shell脚本一个个启动,结果遇到三件糟心事:
- 每次重启机器,容器IP全变了,Nginx配置里的upstream地址得手动改
- PostgreSQL容器一重启,数据全没了——因为没挂载卷
- 服务启动顺序全靠sleep硬等,RabbitMQ还没就绪,Spring Boot已经连不上报错了
后来同事提醒我:这场景就该用Docker Compose编排。折腾了两天,总算把整套配置理顺了。今天把核心配置和踩坑经历写出来,给同样被多服务部署折磨的兄弟一个参考。
2. 环境与版本说明
先交代一下我的运行环境,避免版本差异导致配置不生效:
- 操作系统:Ubuntu 22.04 LTS
- Docker Engine:25.0.3
- Docker Compose:v2.24.6(通过docker compose插件使用,不是老的docker-compose)
- 镜像版本:
- nginx:1.25.4-alpine
- openjdk:17-jdk-slim
- postgres:16.2-alpine
- redis:7.2.4-alpine
- rabbitmq:3.13-management-alpine
注意:Compose文件格式我用的version: '3.8',虽然新版Compose已经弃用version字段了,但为了兼容老项目,我还是习惯写上。如果你用v2.24+,不写version完全没问题。
3. 方案设计:网络、卷、健康检查三位一体
核心思路就三点:
网络隔离:创建一个自定义bridge网络app-network,所有服务都连进去。这样服务间用服务名互相访问,不再依赖IP地址。Nginx对外暴露80端口,其余服务全部内网通信,不映射宿主机端口。
数据持久化:PostgreSQL和Redis的数据目录挂载命名卷。命名卷的好处是即使容器删了重建,数据还在。我见过有人用bind mount挂宿主机路径,结果权限问题折腾半天,命名卷省心多了。
健康检查+启动顺序:这是本次优化的关键。以前用depends_on只控制容器启动先后,但Spring Boot起来时RabbitMQ可能还没就绪。现在用healthcheck配合depends_on: condition: service_healthy,确保前置服务真正可用后才启动下游服务。
整个启动顺序设计成:
postgres → redis → rabbitmq → backend → nginx
(Nginx依赖backend健康,backend依赖三个中间件健康)
4. 核心实现:完整的docker-compose.yml
直接上配置,我把注释写详细点,大家复制改改就能用:
version: '3.8'
# 自定义网络,driver bridge模式
networks:
app-network:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16 # 指定网段,避免和宿主机网段冲突
# 命名卷,数据持久化
volumes:
postgres-data:
driver: local
redis-data:
driver: local
services:
# 1. PostgreSQL数据库
postgres:
image: postgres:16.2-alpine
container_name: app-postgres
restart: unless-stopped
environment:
POSTGRES_USER: app_user
POSTGRES_PASSWORD: app_pass_2024
POSTGRES_DB: app_db
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-sql:/docker-entrypoint-initdb.d # 初始化SQL脚本,只执行一次
networks:
app-network:
ipv4_address: 172.28.0.10 # 固定IP,方便排查问题
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s # 启动宽限期,避免容器刚启动就被标记unhealthy
# 2. Redis缓存
redis:
image: redis:7.2.4-alpine
container_name: app-redis
restart: unless-stopped
command: redis-server --requirepass redis_pass_2024 --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
app-network:
ipv4_address: 172.28.0.11
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis_pass_2024", "ping"]
interval: 10s
timeout: 3s
retries: 5
# 3. RabbitMQ消息队列
rabbitmq:
image: rabbitmq:3.13-management-alpine
container_name: app-rabbitmq
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: mq_user
RABBITMQ_DEFAULT_PASS: mq_pass_2024
networks:
app-network:
ipv4_address: 172.28.0.12
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 15s
timeout: 10s
retries: 5
# 4. Spring Boot后端服务
backend:
image: openjdk:17-jdk-slim
container_name: app-backend
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
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
SPRING_RABBITMQ_HOST: rabbitmq
SPRING_RABBITMQ_USERNAME: mq_user
SPRING_RABBITMQ_PASSWORD: mq_pass_2024
volumes:
- ./backend/app.jar:/app/app.jar # 挂载jar包,方便更新代码
command: ["java", "-Xms512m", "-Xmx1g", "-jar", "/app/app.jar"]
networks:
app-network:
ipv4_address: 172.28.0.13
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 40s # Spring Boot启动慢,给足宽限
# 5. Nginx反向代理
nginx:
image: nginx:1.25.4-alpine
container_name: app-nginx
restart: unless-stopped
depends_on:
backend:
condition: service_healthy
ports:
- "80:80" # 唯一对外暴露的端口
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/logs:/var/log/nginx
networks:
app-network:
ipv4_address: 172.28.0.14
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/healthz"]
interval: 10s
timeout: 3s
retries: 3
对应的Nginx配置nginx/conf.d/app.conf:
upstream backend_pool {
server backend:8080; # 直接使用Compose服务名解析
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
location /api/ {
proxy_pass http://backend_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
location /healthz {
return 200 'ok';
add_header Content-Type text/plain;
}
}
部署命令就三行:
# 构建并启动所有服务(-d后台运行)
docker compose up -d
# 查看服务状态
docker compose ps
# 查看启动日志(调试用)
docker compose logs -f backend
5. 踩坑记录:三个真实教训
坑1:depends_on不加condition等于白写
一开始我只写了depends_on: [postgres, redis, rabbitmq],结果Spring Boot照样报连接拒绝。查了文档才明白,Compose v3默认只保证容器启动顺序,不保证服务就绪。必须显式加condition: service_healthy。另外注意:service_healthy要求目标服务必须定义了healthcheck,否则Compose直接报错。
坑2:PostgreSQL健康检查不能用pg_isready的默认参数
我最初写的健康检查是test: ["CMD-SHELL", "pg_isready"],不加用户名和数据库。结果容器启动后一直显示healthy,但Spring Boot连不上——因为pg_isready默认检查的是当前用户,而容器里默认用户是root,根本不存在。后来改成pg_isready -U app_user -d app_db才准确。
坑3:固定IP和健康检查的start_period要配合调
给每个容器固定IP(ipv4_address)是为了排查问题方便,但有个副作用:如果网段和宿主机冲突,会导致容器网络不通。我第一次用的192.168.1.0/24,结果和办公室WiFi网段冲突,所有容器互ping不通。换成172.28.0.0/16后问题消失。另外start_period必须根据服务实际启动时间调:PostgreSQL设10s,Spring Boot设40s,否则服务还没起来就被标记unhealthy,导致整个链路卡住。
6. 优化效果:从3分半到45秒
改完配置后,我特意做了几组对比测试,数据如下:
启动时间对比(冷启动,清空所有容器和卷)
| 方案 | 总耗时 | 后端服务可用时间 |
|---|---|---|
| docker run + sleep 10 | 3分25秒 | 2分50秒 |
| Compose + depends_on(无condition) | 1分58秒 | 2分01秒(后端仍可能报错) |
| Compose + healthcheck + condition | 44秒 | 42秒 |
稳定性测试(连续重启10次)
- 容器IP变化:0次(固定IP+自定义网络生效)
- 数据丢失:0次(命名卷持久化)
- 后端连接中间件报错:0次(healthcheck确保就绪)
资源占用情况
- 5个容器总内存:1.2GB(backend占700MB,其他都是轻量级)
- 磁盘占用:镜像约800MB,数据卷增长稳定
日常操作也方便了很多。以前更新后端代码得手动停容器、删容器、重新run,现在只需要:
docker compose build backend && docker compose up -d backend
Compose会智能识别只有backend变了,其他容器不动,秒级完成滚动更新。
7. 总结与建议
这套配置用了两个月,没出过大问题。最后给几点建议:
- 生产环境务必给关键服务加restart: unless-stopped,否则服务器重启后容器不会自动拉起
- healthcheck的start_period一定要给足,特别是JVM应用,别省这十几秒
- 敏感信息(密码、密钥)别写死在compose文件里,用环境变量文件
.env,并在.gitignore里排除它 - 固定IP不是必须的,但如果你要配防火墙规则或排查网络问题,固定IP能省不少事
- 升级Compose v2后,
version字段可以删掉,但保留也无妨
这套配置我已经放到GitHub仓库(链接在评论区),有需要的直接拉下来改改镜像名就能用。如果你也踩过其他坑,欢迎评论交流。