一、问题背景:多服务编排的三大痛点

上个月接手一个遗留项目,4个服务(Nginx前端、Java后端、PostgreSQL、Redis)靠一堆shell脚本启动。每次部署都要手动敲命令,顺序错了就报连接拒绝。更头疼的是:

  1. 启动顺序不可控:后端还没等数据库就绪就启动,连接池疯狂重试,日志刷屏
  2. 网络混乱:所有容器都在默认bridge网络,IP地址随机分配,配置里写死IP,一重启就失效
  3. 数据丢失:PostgreSQL数据存在容器可写层,docker rm后数据全没了

这显然是Docker Compose的典型应用场景。但网上教程大多只讲基础用法,真正生产级配置需要解决网络隔离、数据持久化、健康检查、启动依赖这些实际问题。

二、环境与版本

先交代一下我的环境,避免版本差异导致配置不兼容:

  • Docker Engine:24.0.7
  • Docker Compose:v2.24.2(注意,v1和v2的语法略有差异,本文使用v2)
  • 宿主机:Ubuntu 22.04 LTS,4核8G
  • 镜像版本:nginx:1.25.3-alpine、postgres:16.1-alpine、redis:7.2.3-alpine、后端镜像(自建,基于openjdk:17-jdk-alpine)

三、方案设计:三层架构的Compose编排

我的设计思路是:

  1. 网络:创建两个自定义网络。frontend网络只挂Nginx和后端,backend网络挂后端、PostgreSQL、Redis。这样前端无法直达数据库,安全组规则更清晰
  2. :PostgreSQL数据挂到命名卷pg_data,Redis持久化挂到redis_data。后端日志挂到./logs目录,方便宿主机直接查看
  3. 健康检查:三个依赖服务(PostgreSQL、Redis、后端)都配置healthcheck。后端启动前依赖PostgreSQL和Redis健康,Nginx依赖后端健康
  4. 启动顺序:利用depends_oncondition: service_healthy,确保严格按依赖链启动

这个设计的核心思想是:让每个服务只暴露必要的通信路径,数据不丢失,启动不慌

四、核心实现:完整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:
  pg_data:
    driver: local
  redis_data:
    driver: local

services:
  # ---------- 前端入口 ----------
  nginx:
    image: nginx:1.25.3-alpine
    container_name: gateway-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    networks:
      - frontend
    depends_on:
      backend:
        condition: service_healthy
    restart: unless-stopped

  # ---------- 后端API ----------
  backend:
    image: myapp/backend:1.4.2
    container_name: app-backend
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: myapp
      DB_USER: myapp_user
      DB_PASSWORD: ${DB_PASSWORD}  # 从.env文件读取
      REDIS_HOST: redis
      REDIS_PORT: 6379
    volumes:
      - ./logs:/app/logs
    networks:
      - frontend
      - backend
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  # ---------- 数据库 ----------
  postgres:
    image: postgres:16.1-alpine
    container_name: db-postgres
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init-sql:/docker-entrypoint-initdb.d:ro
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp_user -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped

  # ---------- 缓存 ----------
  redis:
    image: redis:7.2.3-alpine
    container_name: cache-redis
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 5s
    restart: unless-stopped

注意几个细节:

  • container_name:固定容器名,方便日志排查和运维脚本
  • ${DB_PASSWORD}:从.env文件读取敏感信息,不要硬编码在YAML里
  • restart: unless-stopped:崩溃自动重启,但手动停止不拉起
  • start_period:给服务预热时间,避免启动慢被误判为不健康

五、踩坑与优化:三个真实问题

坑1:健康检查命令找不到
后端容器基于openjdk:17-jdk-alpine,里面没有curl。第一次配置test: ["CMD", "curl", "-f", "http://localhost:8080/health"]直接报exec: "curl": executable file not found
解决:换用wget(alpine自带)或者改用Java的/actuator/health探测。我最终在Dockerfile里加了RUN apk add --no-cache curl,一劳永逸。

坑2:depends_on只等容器启动,不等服务就绪
depends_on默认只检查容器是否创建成功,不关心服务是否就绪。早期配置depends_on: [postgres, redis],后端启动后连数据库失败。
解决:必须配合condition: service_healthy,让Compose等待健康检查通过才启动依赖方。

坑3:PostgreSQL数据初始化脚本重复执行
我把建表SQL放在./init-sql目录,首次启动执行成功。但后来改了SQL,重启容器发现没执行。
原因:/docker-entrypoint-initdb.d只在数据目录为空时执行。
解决:需要执行docker compose down -v清卷重来,或者手动psql导入。这个坑提醒我生产环境禁止用init-sql做schema变更,应该走Flyway/Liquibase。

优化:启动耗时压测
优化前:纯shell脚本启动,无健康检查,平均需要48秒(后端重试3次才连上DB)。
优化后:用healthcheck + depends_on组合,平均22秒完成全部服务拉起。其中PostgreSQL就绪花了8秒,Redis花了2秒,后端依赖检查通过后12秒完成启动。

六、效果数据:从99.2%到99.9%

部署这套Compose配置后,做了7天连续压测(QPS 2000并发):

指标 优化前 优化后
平均启动时间 48s 22s
服务可用性 99.2% 99.9%
数据丢失事故 2次 0次
部署失败率 15% 0.5%

最明显的改善是部署失败率从15%降到0.5%,以前经常因为启动顺序问题导致后端连不上DB,现在完全是自动化的。

七、总结与建议

这套配置已经稳定运行两个月,总结几点经验:

  1. 自定义网络必须做,哪怕只有两个服务。默认bridge网络无法控制IP,安全性和可维护性都很差
  2. 命名卷是数据安全的底线,尤其是数据库。别图省事用bind mount到宿主机目录,权限问题会烦死你
  3. 健康检查要针对业务设计pg_isreadyredis-cli pingcurl /actuator/health都是标准做法。不要用CMD-SHELLping容器IP,那只能证明网络通,不能证明服务可用
  4. .env文件管理密钥,不要提交到Git仓库。docker compose config可以验证变量是否解析正确

如果你也在做微服务多容器部署,建议直接复制这份配置改改服务名和镜像版本。遇到问题可以评论区交流。


附:常用命令速查

# 启动所有服务
docker compose up -d

# 查看服务状态(显示健康检查结果)
docker compose ps

# 查看日志(实时跟踪)
docker compose logs -f backend

# 重新构建并启动(代码变更后)
docker compose up -d --build

# 停止并删除容器(保留卷)
docker compose down

# 停止并删除容器+卷(危险,数据全没)
docker compose down -v