一、问题背景:手工docker run管理多服务的痛

上个月接手一个老项目,6个服务靠一堆shell脚本启动,每次部署都要手动敲docker run,端口冲突、依赖顺序错乱、数据丢失是家常便饭。尤其有一次Redis先于PostgreSQL启动,应用连不上库直接崩溃,排查了半天才发现是启动顺序问题。

后来决定用Docker Compose重构,但网上教程大多只讲单机单服务,真正生产级的多服务编排,网络、卷、健康检查这些关键点都语焉不详。这篇文章就分享下我实际落地的配置,踩过的坑和优化后的最终方案。

二、环境与版本

Docker Engine: 24.0.7
Docker Compose: v2.24.2
操作系统: Ubuntu 22.04 LTS
服务器: 4核8G 阿里云ECS

项目组成:
- nginx:前端静态资源 + 反向代理,版本1.25.3
- backend:Spring Boot 3.2.0,打包成jar
- postgres:业务数据库,版本16.1
- redis:缓存,版本7.2.3

三、方案设计:网络隔离 + 依赖控制

先说设计思路。Compose文件我拆成两部分:docker-compose.yml(基础配置)和docker-compose.prod.yml(生产覆盖)。核心要解决三个问题:

1. 网络隔离:所有服务挂在同一个自定义bridge网络app_net下,服务间通过服务名通信(Compose内置DNS解析),外部只能通过Nginx暴露80端口。

2. 数据持久化:PostgreSQL和Redis用命名卷(named volume)存储数据,避免容器删除后数据丢失。注意不是bind mount,bind mount依赖宿主机目录结构,可移植性差。

3. 启动顺序与健康检查:这是最关键的部分。Compose的depends_on在2.x版本默认只控制启动顺序,不等待服务就绪。必须配合healthcheck才能实现“等数据库真正可用了再启动后端”。

四、核心实现:完整docker-compose配置

直接上配置,这是我在生产环境跑的版本,去掉了敏感信息:

# docker-compose.yml
version: "3.8"

services:
  nginx:
    image: nginx:1.25.3-alpine
    container_name: nginx-gateway
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./dist:/usr/share/nginx/html:ro
    networks:
      - app_net
    depends_on:
      backend:
        condition: service_healthy
    restart: always

  backend:
    build: ./backend
    image: myapp/backend:1.0.0
    container_name: backend-service
    expose:
      - "8080"  # 仅内部网络可访问
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DB_HOST=postgres
      - REDIS_HOST=redis
    volumes:
      - ./logs:/app/logs
    networks:
      - app_net
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 40s
    restart: always

  postgres:
    image: postgres:16.1-alpine
    container_name: postgres-db
    environment:
      - POSTGRES_USER=myapp
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=myapp_db
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./backup:/backup
    networks:
      - app_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp -d myapp_db"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: always

  redis:
    image: redis:7.2.3-alpine
    container_name: redis-cache
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    networks:
      - app_net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: always

networks:
  app_net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

volumes:
  pg_data:
    driver: local
  redis_data:
    driver: local

这里有几个细节必须说明:

  • expose vs ports:backend只对内部网络暴露8080,不映射到宿主机。Nginx通过http://backend:8080反向代理,安全且减少端口占用。
  • 健康检查命令:PostgreSQL用pg_isready,Redis用redis-cli ping,Spring Boot用curl打actuator端点。注意Spring Boot镜像里可能没有curl,我是在Dockerfile里装的。
  • start_period参数:给JVM启动留了40秒缓冲,避免启动慢导致误判为不健康。

生产环境覆盖文件:

# docker-compose.prod.yml
version: "3.8"

services:
  backend:
    environment:
      - JAVA_OPTS=-Xmx2g -Xms2g
      - DB_PASSWORD=${DB_PASSWORD}
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 3g
        reservations:
          cpus: "0.5"
          memory: 1g

  postgres:
    deploy:
      resources:
        limits:
          memory: 2g

五、踩坑与优化:三个真实教训

坑1:depends_on不等待就绪

最开始我写的是:

depends_on:
  - postgres
  - redis

结果后端容器在PostgreSQL还没初始化完就启动,连不上库直接退出。后来改成condition: service_healthy才解决。注意只有Compose 2.x才支持这个语法,老版本1.x只能用wait-for-it.sh脚本。

坑2:健康检查命令不存在

Spring Boot的健康检查我一开始用的是wget -qO- http://localhost:8080/actuator/health,结果容器一直报unhealthy。查了半天发现基础镜像eclipse-temurin:17-jre-alpine里没有wget。换成curl后需要额外安装,我直接在Dockerfile里加了一行:

RUN apk add --no-cache curl

坑3:卷挂载权限问题

PostgreSQL数据卷一开始用的bind mount(./pgdata:/var/lib/postgresql/data),结果宿主机目录权限不对,容器启动报错。改成命名卷后彻底解决,Compose自动管理权限,而且docker volume ls能看到数据独立于容器生命周期。

优化:并行启动提速

Compose默认并行启动无依赖关系的服务。优化后,nginx、postgres、redis同时启动,backend等健康检查通过后启动。实测完整启动时间从串行的约400秒(含JVM启动和数据库初始化)降到120秒,减少了70%。

六、效果数据与验证

部署到4核8G机器后,我用docker compose ps观察状态:

NAME            IMAGE                          STATUS
nginx-gateway   nginx:1.25.3-alpine            Up 2 hours (healthy)
backend-service myapp/backend:1.0.0            Up 2 hours (healthy)
postgres-db     postgres:16.1-alpine           Up 2 hours (healthy)
redis-cache     redis:7.2.3-alpine             Up 2 hours (healthy)

实际压测数据(wrk,100并发,30秒):
- 接口平均响应时间 86ms,P99 210ms
- 容器重启恢复:模拟docker compose restart backend,从停止到健康状态耗时约28秒(JVM启动20秒 + 健康检查8秒)
- 数据持久化验证:docker compose down后再up,PostgreSQL数据完整,Redis缓存键未丢失

七、总结与建议

这套配置已经稳定运行3周,部署效率提升明显。给几点建议:

  1. 网络一定要自定义,别用默认的bridge网络,服务名解析和隔离性都更可控。
  2. 健康检查别省,这是控制依赖序的关键,比任何wait-for-it脚本都可靠。
  3. 生产环境务必用命名卷,数据安全是底线。
  4. restart: always加上,配合健康检查,容器挂了能自动恢复,配合运维告警能做到无人值守。

下一步我准备把Compose配置迁移到Docker Swarm或K8s,但那是另一个故事了。有问题欢迎评论区交流。