先说下背景。我们团队接手了一个老项目,原来本地开发要在本机装MySQL、Redis、RabbitMQ三件套,每次新同事入职光配环境就要一下午。Docker单容器跑倒是没问题,但服务一多(前端网关、后端API、消息队列、缓存),docker run命令写起来又臭又长,而且容器重启后IP变化导致配置失效,非常痛苦。
我决定用Docker Compose统一编排。目标很明确:一条docker compose up -d命令拉起全部服务,开发、测试、生产三套环境共用同一份编排逻辑。实际做下来发现,真正难的不是写compose文件,而是处理网络隔离、健康检查、启动顺序这些细节。
环境与版本
先说版本,这些坑都是踩出来的:
- Docker Engine: 24.0.7(之前用的20.10,network别名解析有bug)
- Docker Compose: v2.24.2(v1的depends_on条件不支持,必须升到v2)
- 宿主机: Ubuntu 22.04 LTS,内核5.15
- 项目技术栈: Spring Boot 3.2.1,PostgreSQL 14,Redis 7.2,RabbitMQ 3.12
方案设计
整体架构分三层。外层是Nginx网关(端口8080),中间是Spring Boot应用集群(内部端口8080,不暴露),底层是Redis、PostgreSQL、RabbitMQ三个基础设施服务。
网络设计采用两个自定义bridge网络:
- frontend_net:仅Nginx和Spring Boot应用接入,用于反向代理通信
- backend_net:Spring Boot应用和三个基础设施服务接入,用于数据访问
为什么要分两个?隔离。如果所有服务都在同一个网络,任何被攻破的容器都能直接访问数据库。拆开后,外部请求只能打到Nginx,数据库完全在内网。这个设计参考了Docker官方文档的多网络最佳实践。
启动顺序控制是另一个难点。Spring Boot启动时如果连不上数据库会直接报错退出,所以必须等PostgreSQL和Redis就绪后才能启动应用。Compose v2的depends_on支持condition: service_healthy,配合healthcheck使用。
核心实现
先看完整的docker-compose.yml文件(这是第一版,踩坑后的优化版在后面):
version: "3.9"
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
volumes:
postgres_data:
driver: local
redis_data:
driver: local
services:
nginx:
image: nginx:1.25-alpine
container_name: api-gateway
ports:
- "8080:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/logs:/var/log/nginx
networks:
- frontend_net
depends_on:
- app
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
app:
build: ./app
image: myapp:1.0.0
container_name: spring-app
env_file:
- ./app/.env
environment:
SPRING_PROFILES_ACTIVE: docker
DB_HOST: postgres
DB_PORT: 5432
REDIS_HOST: redis
RABBITMQ_HOST: rabbitmq
volumes:
- ./app/logs:/app/logs
networks:
- frontend_net
- backend_net
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 15s
timeout: 10s
retries: 5
start_period: 40s
postgres:
image: postgres:14.10-alpine
container_name: postgres-db
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
networks:
- backend_net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7.2-alpine
container_name: redis-cache
command: redis-server --appendonly yes --requirepass redispass
volumes:
- redis_data:/data
networks:
- backend_net
healthcheck:
test: ["CMD", "redis-cli", "-a", "redispass", "ping"]
interval: 5s
timeout: 3s
retries: 10
rabbitmq:
image: rabbitmq:3.12-management-alpine
container_name: rabbit-mq
environment:
RABBITMQ_DEFAULT_USER: mquser
RABBITMQ_DEFAULT_PASS: mqpass
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- backend_net
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 5
注意几个关键点。PostgreSQL使用命名卷postgres_data持久化数据,而应用日志用bind mount挂载到宿主机./app/logs目录,便于开发时直接查看日志文件。Redis开了AOF持久化(appendonly yes),数据存到redis_data卷。
healthcheck的写法有讲究。PostgreSQL用pg_isready命令,这个命令在postgres镜像里自带,不需要额外安装。Redis用redis-cli ping,注意密码验证方式,-a参数指定密码,否则健康检查会一直失败。RabbitMQ的rabbitmq-diagnostics ping是官方推荐方式。
踩坑与优化
第一个坑:depends_on顺序。我们一开始写的是:
depends_on:
- postgres
- redis
- rabbitmq
这只保证启动顺序,不保证就绪状态。Spring Boot容器启动了,但PostgreSQL还在初始化,应用连接失败直接崩溃。后来改成条件依赖:
depends_on:
postgres:
condition: service_healthy
但这里有个坑:Spring Boot的start_period设置成40秒,如果超过40秒还没通过健康检查,Compose会标记为unhealthy并重启容器。PostgreSQL第一次初始化需要建表跑init脚本,大概需要15秒左右,40秒是够的。
第二个坑:网络别名。Docker Compose默认用服务名作为DNS解析,但如果你在自定义网络里指定了ipam的subnet,容器IP会按顺序分配。之前用Docker 20.10时,偶尔出现容器重启后IP变化,导致Nginx配置里的upstream失效。升级到24.0.7后这个问题解决了,因为Compose v2强制用服务名解析,不再依赖IP。
第三个坑:健康检查的超时设置。Redis的healthcheck我最初没加-a参数,结果密码校验失败,健康检查一直不通过,整个编排卡住。后来改成:
test: ["CMD", "redis-cli", "-a", "redispass", "ping"]
这里注意,版本号redis:7.2-alpine的redis-cli支持-a参数,但老版本可能不支持,建议用REDISCLI_AUTH环境变量替代。
优化后的最终版本(第二版)做了两个改动:
- 给Nginx的healthcheck加了start_period,避免Nginx启动慢导致误判
- 把Spring Boot的启动依赖改成两种条件混合:
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
nginx:
condition: service_started
nginx用service_started而不是service_healthy,因为Nginx是网关,只要进程启动就能转发请求,不需要等它自己健康检查通过。
效果数据
优化前后对比:
- 服务全部就绪时间:从95秒(无健康检查,依赖纯顺序)降到35秒(有健康检查+条件依赖)
- 容器重启恢复时间:单容器崩溃后,Compose自动拉起并等待依赖就绪,平均恢复时间从3分钟降到45秒
- 开发环境搭建时间:新同事从下载依赖到跑通全部服务,从2小时降到15分钟(前提是装好Docker)
- 网络隔离效果:安全扫描发现,暴露的端口从8个降到1个(只有Nginx的8080)
还有一个量化数据:用docker compose config --services确认服务定义无误,docker compose ps显示所有容器healthy状态。生产环境用docker compose --env-file .env.prod up -d切换配置,这也是Compose v2支持的功能。
总结
Docker Compose编排多服务,核心就三件事:网络要隔离、服务要健康检查、启动要控制顺序。这三件事做对了,容器化部署就成功了一大半。
踩过最深的坑是depends_on只保证启动顺序不保证就绪状态,这个坑估计90%的人都会踩。另外健康检查的命令一定要和服务镜像匹配,比如Redis的密码参数、PostgreSQL的用户名,这些细节不处理好,Compose会一直等待导致整个项目卡住。
最后给个建议:如果你也在做容器化改造,从Compose v2开始,别用v1了。v2的depends_on条件、--env-file、docker compose config校验这些特性,能省不少事。版本选新不选旧,Docker Engine至少24.x,Compose至少v2.20。
整个配置文件我已经放在项目仓库里,有需要的可以自取。有问题欢迎评论区交流。