一、为什么我需要重新审视容器编排
事情发生在两周前。我们团队维护的电商后端项目,包含一个Spring Boot应用、PostgreSQL、Redis、RabbitMQ和Nginx。之前一直是手工docker run,每次部署要敲6条命令,还要注意启动顺序——必须先起数据库,等它初始化完,再起应用,否则应用启动时连不上数据库直接崩溃。
这周上线新功能,需要新增一个消息消费者服务。我算了一笔账:6条docker run变成8条,启动顺序更复杂,还要处理容器间网络通信。更糟糕的是,上周一次部署中,因为Redis容器还没就绪,Spring Boot应用就启动了,导致缓存连接池初始化失败,线上出现5分钟服务不可用。
我意识到,必须用Docker Compose来管理这些容器了。目标是:一条命令启动全部服务,自动处理依赖关系和健康检查。
二、环境与版本
先交代一下我的测试和生产环境:
Docker: 24.0.7
Docker Compose: v2.20.2
操作系统: Ubuntu 22.04 LTS (生产) / macOS 14.1 (开发)
Spring Boot: 3.1.5
PostgreSQL: 16.1 (镜像 postgres:16.1-alpine)
Redis: 7.2.3 (镜像 redis:7.2-alpine)
RabbitMQ: 3.12.10 (镜像 rabbitmq:3.12-management-alpine)
Nginx: 1.25.3 (镜像 nginx:1.25.3-alpine)
注意,我特意选了alpine版本,镜像体积平均减少约60%。比如postgres:16.1-alpine只有约250MB,而完整版是400MB+。对于内网部署,这省下的带宽和时间很可观。
三、方案设计:网络、卷、健康检查、启动顺序
我的设计考虑四个维度:
1. 网络隔离:创建两个bridge网络——frontend和backend。Nginx暴露80端口,通过frontend网络访问Spring Boot应用;Spring Boot通过backend网络访问PostgreSQL、Redis和RabbitMQ。这样即使某个容器被攻破,也无法直接访问数据库(因为不在同一个网络)。
2. 数据持久化:PostgreSQL数据目录挂载到宿主机/data/postgres,Redis持久化文件挂载到/data/redis。RabbitMQ的消息队列数据挂载到/data/rabbitmq。这样容器重建后数据不丢失。
3. 健康检查:每个依赖服务都定义healthcheck:
- PostgreSQL:使用pg_isready -U user -d dbname,每5秒检查一次
- Redis:使用redis-cli ping,期望返回PONG
- RabbitMQ:使用rabbitmq-diagnostics -q ping,每10秒检查一次
4. 启动顺序:depends_on配合condition: service_healthy。Spring Boot应用会等待PostgreSQL、Redis、RabbitMQ都健康后才启动。Nginx等待应用健康后启动。
四、核心实现:docker-compose.yml完整配置
以下是完整的docker-compose.yml文件,我加了详细的注释:
version: "3.8"
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
backend:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/24
volumes:
postgres_data:
driver: local
redis_data:
driver: local
rabbitmq_data:
driver: local
services:
postgres:
image: postgres:16.1-alpine
container_name: ecommerce-postgres
restart: always
environment:
POSTGRES_USER: ecommerce_user
POSTGRES_PASSWORD: ${DB_PASSWORD} # 从.env文件读取
POSTGRES_DB: ecommerce_db
ports:
- "5432:5432" # 仅开发环境暴露,生产应去掉
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro # 初始化SQL脚本
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ecommerce_user -d ecommerce_db"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
redis:
image: redis:7.2-alpine
container_name: ecommerce-redis
restart: always
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
volumes:
- redis_data:/data
networks:
- backend
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: always
environment:
RABBITMQ_DEFAULT_USER: ecommerce_mq
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
ports:
- "15672:15672" # 管理界面
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- backend
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
app:
build: ./app # 使用Dockerfile构建Spring Boot应用
container_name: ecommerce-app
restart: always
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: production
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ecommerce_db
DB_USER: ecommerce_user
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
RABBITMQ_HOST: rabbitmq
RABBITMQ_PORT: 5672
RABBITMQ_USER: ecommerce_mq
RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD}
networks:
- backend
- frontend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
nginx:
image: nginx:1.25.3-alpine
container_name: ecommerce-nginx
restart: always
depends_on:
app:
condition: service_healthy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./ssl:/etc/nginx/ssl:ro # SSL证书目录
networks:
- frontend
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 3
五、踩坑与优化记录
坑1:healthcheck中的shell变量展开问题
在Redis的healthcheck中,我最初写的是:
test: ["CMD", "redis-cli", "-a", "$REDIS_PASSWORD", "ping"]
结果发现$REDIS_PASSWORD没被展开。Docker Compose的healthcheck不会自动加载.env文件中的变量。正确方式是在command中引用,或者在healthcheck中用CMD-SHELL:
healthcheck:
test: ["CMD-SHELL", "redis-cli -a $REDIS_PASSWORD ping | grep PONG"]
坑2:Spring Boot应用启动慢导致健康检查失败
Spring Boot应用首次启动需要约30秒(内网拉取依赖慢),但我的start_period只设了20秒。结果应用还没起来,健康检查就判定失败,Nginx一直等待。解决方案:
- 将start_period调整为60秒
- 在应用Dockerfile中优化依赖缓存,减少启动时间
坑3:PostgreSQL初始化脚本执行顺序
我把初始化SQL放在/docker-entrypoint-initdb.d目录,但发现如果脚本里有多个.sql文件,按字母顺序执行。我命名为01-schema.sql、02-data.sql,确保先建表再插数据。
优化:利用Compose的缓存加速构建
在Spring Boot的Dockerfile中,我分两步构建:
FROM maven:3.9.6-eclipse-temurin-21 AS builder
COPY pom.xml .
RUN mvn dependency:go-offline -B # 先缓存依赖
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
这样依赖层可以被Docker缓存,后续代码变更构建时间从3分钟降到40秒。
六、效果数据对比
| 部署方式 | 平均部署时间 | 服务可用性 | 故障恢复时间 |
|---|---|---|---|
| 手工docker run | 8分30秒 | 99.2% | 15分钟+ |
| Docker Compose | 1分30秒 | 99.95% | 3分钟 |
具体提升点:
- 启动顺序由人工判断改为自动化,消除了因依赖未就绪导致的启动失败(上线周故障次数从3次降为0)
- 健康检查让Nginx自动等待应用就绪,不再出现502错误
- 数据卷挂载让容器重建时间从15分钟(手动迁移数据)降到30秒(直接重启)
总结
Docker Compose不是万能的,但它解决了我90%的编排痛点。对于中小型多服务项目,它足够清晰、易维护。如果你有Kubernetes的需求,Compose的配置也能平滑迁移到Helm Chart——网络、健康检查、依赖关系的概念是相通的。
最后提醒一点:永远用docker compose config验证你的配置,再docker compose up -d。我在生产环境犯过最大的错就是跳过验证直接部署,结果YAML缩进错误导致整个服务栈起不来,教训深刻。
如果你也在用Compose编排,遇到什么坑,欢迎评论区交流。