一、问题背景:docker run的失控时刻

上个月我负责的一个电商后台项目终于要上测试环境了。按照惯例,我准备了一堆docker run命令——Nginx一个、后端两个实例、PostgreSQL一个、Redis一个、RabbitMQ一个。结果第一天就翻车了:后端容器启动时数据库还没就绪,连接池初始化直接抛异常,Spring Boot启动失败后容器退出,Docker重启策略又拉起来,但数据库这时候刚好在初始化……循环了七八次,日志刷屏,最后我不得不手动清掉所有容器重新来。

这还不是最痛苦的。每次改配置、换端口、调卷路径,都要在一堆历史命令里找半天。更别提测试环境要多副本,我得复制粘贴改端口——这根本不是人干的活。

二、环境与版本:别再踩Compose v2的坑

先交代一下我的环境,避免版本不一致导致配置不生效:

  • Docker Engine:24.0.7(Linux内核 5.15.0)
  • Docker Compose:v2.24.2(注意,v1的version:字段已废弃,不要再写)
  • 宿主机:Ubuntu 22.04 LTS,8核16G
  • 镜像版本:nginx:1.25-alpine、postgres:16.1-alpine、redis:7.2-alpine、rabbitmq:3.12-management-alpine、openjdk:17-jdk-slim(后端镜像基于此构建)

这里强调一下,Compose v2默认就是docker compose(中间有空格),不再是docker-compose。另外,depends_oncondition字段在v2中终于支持service_healthy了,这是解决启动顺序的关键。

三、方案设计:三层网络 + 显式健康检查

我的设计思路分三部分:

1. 自定义网络隔离
默认的bridge网络所有容器互通,但生产环境我习惯分三层:
- frontend_net:只有Nginx暴露80端口,承接外部流量
- backend_net:后端服务与中间件通信
- data_net:数据库、Redis、RabbitMQ之间隔离,避免前端容器直接碰数据库

2. 卷挂载策略
- PostgreSQL数据目录挂到宿主机/data/postgres——容器删了数据还在,这不用多说
- 后端日志目录挂载./logs/app:/logs,方便开发直接看日志文件
- Nginx静态资源挂载./web:/usr/share/nginx/html,前端发版直接替换文件

3. 启动顺序控制
核心逻辑:depends_on + condition: service_healthy。每个中间件必须通过健康检查,后端容器才启动;Nginx最后启动,确保后端真正ready后才接入流量。

四、核心实现:docker-compose.yml全解

直接上配置,这是我家底:

version: "3.9"  # 兼容性声明,v2其实会忽略,但保留无妨

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
  data_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.0.0/24

volumes:
  postgres_data:
    driver: local
  rabbitmq_data:
    driver: local

services:
  postgres:
    image: postgres:16.1-alpine
    container_name: ecommerce-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: ecommerce
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: ${DB_PASSWORD}  # 从.env读取
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-sql:/docker-entrypoint-initdb.d:ro  # 首次初始化脚本
    networks:
      - data_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d ecommerce"]
      interval: 5s
      timeout: 3s
      retries: 12
      start_period: 10s

  redis:
    image: redis:7.2-alpine
    container_name: ecommerce-redis
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
    volumes:
      - ./redis-data:/data
    networks:
      - data_net
    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: unless-stopped
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    networks:
      - data_net
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 20s  # RabbitMQ启动慢,给足时间

  backend:
    build: ./backend
    image: ecommerce-backend:1.0
    container_name: ecommerce-backend-1
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    environment:
      SPRING_PROFILES_ACTIVE: docker
      DB_HOST: postgres
      DB_PORT: 5432
      REDIS_HOST: redis
      REDIS_PORT: 6379
      RABBIT_HOST: rabbitmq
      RABBIT_PORT: 5672
    volumes:
      - ./logs/app:/logs
    networks:
      - backend_net
      - data_net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  nginx:
    image: nginx:1.25-alpine
    container_name: ecommerce-nginx
    restart: unless-stopped
    depends_on:
      backend:
        condition: service_healthy
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./web:/usr/share/nginx/html:ro
    networks:
      - frontend_net
      - backend_net

注意几个细节:

  • start_period很关键——PostgreSQL首次初始化可能要15-20秒,如果设置太短,健康检查会在初始化期间误报失败,导致后端等不到就绪就启动。
  • 后端健康检查用的是Spring Boot Actuator的/actuator/health,需要在项目里加上依赖。
  • 密码从.env文件读取,不写死在yml里。.env格式:DB_PASSWORD=yourpass,并确保.gitignore忽略它。

五、踩坑与优化:从90秒到45秒

坑1:depends_on不加condition等于白写
Compose v2默认的depends_on只是启动顺序,不等待服务就绪。我第一次写的时候只写了depends_on: [postgres, redis, rabbitmq],结果后端照样连不上数据库。必须显式加condition: service_healthy

坑2:RabbitMQ健康检查超时设置太短
RabbitMQ容器起来后,管理插件初始化需要时间。如果start_period给10秒,retries给3次,每次间隔5秒,总共25秒——在低配机器上大概率失败。我调到start_period: 20sretries: 6,终于稳定。

坑3:Nginx反代后端,连接超时
Nginx默认proxy_read_timeout 60s,但后端有接口要跑80秒(导出Excel报表),直接504。在conf里加:

location /api/ {
    proxy_pass http://backend:8080;
    proxy_connect_timeout 5s;
    proxy_read_timeout 120s;
}

优化:合并健康检查,缩短冷启动
之前的配置里,PostgreSQL和Redis健康检查间隔都是10秒,最坏情况要等10秒才探测一次。我把间隔统一改为5秒,start_period精确到服务实际初始化时间,通过docker compose logs -f观察日志,反复调整后整体冷启动从90秒缩短到45秒。

做个小对比:

项目 优化前 优化后
PostgreSQL就绪 22s 15s(start_period精准)
后端启动 35s(含等待) 20s(依赖条件触发)
总耗时 90s+ 45s
容器重启次数 3-5次 0次

六、效果数据与总结

这套配置在测试环境跑了三周,20+个功能模块迭代,容器零意外重启。最直观的变化:

  • 部署时间:从手动敲15条命令约10分钟,变成docker compose up -d一条命令2分钟搞定
  • 故障恢复:宿主机重启后,restart: unless-stopped自动拉起所有服务,健康检查失败自动重启,无需人工干预
  • 日志排查docker compose logs -f backend --tail=100直接看后端日志,不用再docker logs -f 容器ID

最后给个建议:别在Compose文件里写死版本号以外的配置,环境差异用.env管理;健康检查的start_period一定要根据实际服务启动时间调,宁长勿短;网络划分别图省事全塞一个brige里,隔离性差不说,排查问题也不方便。这套配置我放到了公司GitLab的CI里,每次代码合并自动构建镜像并docker compose up -d,测试同学再也没因为环境问题找我聊过天。