1. 问题背景:docker run脚本化部署的痛点

上个月,我把一个电商后台拆成了5个服务:Nginx做反向代理,Spring Boot跑业务逻辑,PostgreSQL存订单,Redis扛缓存,RabbitMQ处理异步消息。刚开始用shell脚本一个个启动,结果遇到三件糟心事:

  • 每次重启机器,容器IP全变了,Nginx配置里的upstream地址得手动改
  • PostgreSQL容器一重启,数据全没了——因为没挂载卷
  • 服务启动顺序全靠sleep硬等,RabbitMQ还没就绪,Spring Boot已经连不上报错了

后来同事提醒我:这场景就该用Docker Compose编排。折腾了两天,总算把整套配置理顺了。今天把核心配置和踩坑经历写出来,给同样被多服务部署折磨的兄弟一个参考。

2. 环境与版本说明

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

  • 操作系统:Ubuntu 22.04 LTS
  • Docker Engine:25.0.3
  • Docker Compose:v2.24.6(通过docker compose插件使用,不是老的docker-compose)
  • 镜像版本:
  • nginx:1.25.4-alpine
  • openjdk:17-jdk-slim
  • postgres:16.2-alpine
  • redis:7.2.4-alpine
  • rabbitmq:3.13-management-alpine

注意:Compose文件格式我用的version: '3.8',虽然新版Compose已经弃用version字段了,但为了兼容老项目,我还是习惯写上。如果你用v2.24+,不写version完全没问题。

3. 方案设计:网络、卷、健康检查三位一体

核心思路就三点:

网络隔离:创建一个自定义bridge网络app-network,所有服务都连进去。这样服务间用服务名互相访问,不再依赖IP地址。Nginx对外暴露80端口,其余服务全部内网通信,不映射宿主机端口。

数据持久化:PostgreSQL和Redis的数据目录挂载命名卷。命名卷的好处是即使容器删了重建,数据还在。我见过有人用bind mount挂宿主机路径,结果权限问题折腾半天,命名卷省心多了。

健康检查+启动顺序:这是本次优化的关键。以前用depends_on只控制容器启动先后,但Spring Boot起来时RabbitMQ可能还没就绪。现在用healthcheck配合depends_on: condition: service_healthy,确保前置服务真正可用后才启动下游服务。

整个启动顺序设计成:

postgres → redis → rabbitmq → backend → nginx

(Nginx依赖backend健康,backend依赖三个中间件健康)

4. 核心实现:完整的docker-compose.yml

直接上配置,我把注释写详细点,大家复制改改就能用:

version: '3.8'

# 自定义网络,driver bridge模式
networks:
  app-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16   # 指定网段,避免和宿主机网段冲突

# 命名卷,数据持久化
volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local

services:
  # 1. PostgreSQL数据库
  postgres:
    image: postgres:16.2-alpine
    container_name: app-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: app_pass_2024
      POSTGRES_DB: app_db
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-sql:/docker-entrypoint-initdb.d  # 初始化SQL脚本,只执行一次
    networks:
      app-network:
        ipv4_address: 172.28.0.10   # 固定IP,方便排查问题
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d app_db"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s   # 启动宽限期,避免容器刚启动就被标记unhealthy

  # 2. Redis缓存
  redis:
    image: redis:7.2.4-alpine
    container_name: app-redis
    restart: unless-stopped
    command: redis-server --requirepass redis_pass_2024 --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    networks:
      app-network:
        ipv4_address: 172.28.0.11
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redis_pass_2024", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

  # 3. RabbitMQ消息队列
  rabbitmq:
    image: rabbitmq:3.13-management-alpine
    container_name: app-rabbitmq
    restart: unless-stopped
    environment:
      RABBITMQ_DEFAULT_USER: mq_user
      RABBITMQ_DEFAULT_PASS: mq_pass_2024
    networks:
      app-network:
        ipv4_address: 172.28.0.12
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 15s
      timeout: 10s
      retries: 5

  # 4. Spring Boot后端服务
  backend:
    image: openjdk:17-jdk-slim
    container_name: app-backend
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/app_db
      SPRING_DATASOURCE_USERNAME: app_user
      SPRING_DATASOURCE_PASSWORD: app_pass_2024
      SPRING_DATA_REDIS_HOST: redis
      SPRING_DATA_REDIS_PASSWORD: redis_pass_2024
      SPRING_RABBITMQ_HOST: rabbitmq
      SPRING_RABBITMQ_USERNAME: mq_user
      SPRING_RABBITMQ_PASSWORD: mq_pass_2024
    volumes:
      - ./backend/app.jar:/app/app.jar   # 挂载jar包,方便更新代码
    command: ["java", "-Xms512m", "-Xmx1g", "-jar", "/app/app.jar"]
    networks:
      app-network:
        ipv4_address: 172.28.0.13
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 40s   # Spring Boot启动慢,给足宽限

  # 5. Nginx反向代理
  nginx:
    image: nginx:1.25.4-alpine
    container_name: app-nginx
    restart: unless-stopped
    depends_on:
      backend:
        condition: service_healthy
    ports:
      - "80:80"   # 唯一对外暴露的端口
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/logs:/var/log/nginx
    networks:
      app-network:
        ipv4_address: 172.28.0.14
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost/healthz"]
      interval: 10s
      timeout: 3s
      retries: 3

对应的Nginx配置nginx/conf.d/app.conf

upstream backend_pool {
    server backend:8080;   # 直接使用Compose服务名解析
    keepalive 32;
}

server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        proxy_pass http://backend_pool;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
    }

    location /healthz {
        return 200 'ok';
        add_header Content-Type text/plain;
    }
}

部署命令就三行:

# 构建并启动所有服务(-d后台运行)
docker compose up -d

# 查看服务状态
docker compose ps

# 查看启动日志(调试用)
docker compose logs -f backend

5. 踩坑记录:三个真实教训

坑1:depends_on不加condition等于白写

一开始我只写了depends_on: [postgres, redis, rabbitmq],结果Spring Boot照样报连接拒绝。查了文档才明白,Compose v3默认只保证容器启动顺序,不保证服务就绪。必须显式加condition: service_healthy。另外注意:service_healthy要求目标服务必须定义了healthcheck,否则Compose直接报错。

坑2:PostgreSQL健康检查不能用pg_isready的默认参数

我最初写的健康检查是test: ["CMD-SHELL", "pg_isready"],不加用户名和数据库。结果容器启动后一直显示healthy,但Spring Boot连不上——因为pg_isready默认检查的是当前用户,而容器里默认用户是root,根本不存在。后来改成pg_isready -U app_user -d app_db才准确。

坑3:固定IP和健康检查的start_period要配合调

给每个容器固定IP(ipv4_address)是为了排查问题方便,但有个副作用:如果网段和宿主机冲突,会导致容器网络不通。我第一次用的192.168.1.0/24,结果和办公室WiFi网段冲突,所有容器互ping不通。换成172.28.0.0/16后问题消失。另外start_period必须根据服务实际启动时间调:PostgreSQL设10s,Spring Boot设40s,否则服务还没起来就被标记unhealthy,导致整个链路卡住。

6. 优化效果:从3分半到45秒

改完配置后,我特意做了几组对比测试,数据如下:

启动时间对比(冷启动,清空所有容器和卷)

方案 总耗时 后端服务可用时间
docker run + sleep 10 3分25秒 2分50秒
Compose + depends_on(无condition) 1分58秒 2分01秒(后端仍可能报错)
Compose + healthcheck + condition 44秒 42秒

稳定性测试(连续重启10次)

  • 容器IP变化:0次(固定IP+自定义网络生效)
  • 数据丢失:0次(命名卷持久化)
  • 后端连接中间件报错:0次(healthcheck确保就绪)

资源占用情况

  • 5个容器总内存:1.2GB(backend占700MB,其他都是轻量级)
  • 磁盘占用:镜像约800MB,数据卷增长稳定

日常操作也方便了很多。以前更新后端代码得手动停容器、删容器、重新run,现在只需要:

docker compose build backend && docker compose up -d backend

Compose会智能识别只有backend变了,其他容器不动,秒级完成滚动更新。

7. 总结与建议

这套配置用了两个月,没出过大问题。最后给几点建议:

  1. 生产环境务必给关键服务加restart: unless-stopped,否则服务器重启后容器不会自动拉起
  2. healthcheck的start_period一定要给足,特别是JVM应用,别省这十几秒
  3. 敏感信息(密码、密钥)别写死在compose文件里,用环境变量文件.env,并在.gitignore里排除它
  4. 固定IP不是必须的,但如果你要配防火墙规则或排查网络问题,固定IP能省不少事
  5. 升级Compose v2后,version字段可以删掉,但保留也无妨

这套配置我已经放到GitHub仓库(链接在评论区),有需要的直接拉下来改改镜像名就能用。如果你也踩过其他坑,欢迎评论交流。