一、为什么我需要重新审视容器编排

事情发生在两周前。我们团队维护的电商后端项目,包含一个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网络——frontendbackend。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.sql02-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编排,遇到什么坑,欢迎评论区交流。