先说下背景。我们团队接手了一个老项目,原来本地开发要在本机装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环境变量替代。

优化后的最终版本(第二版)做了两个改动:

  1. 给Nginx的healthcheck加了start_period,避免Nginx启动慢导致误判
  2. 把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。

整个配置文件我已经放在项目仓库里,有需要的可以自取。有问题欢迎评论区交流。