一、问题背景

上个月接手一个遗留项目,四个服务手动docker run启动,每次发版都要盯着终端等日志。最头疼的是Spring Boot应用启动时连不上PostgreSQL,重启三次才能成功——因为数据库容器起来了但还没就绪,应用已经尝试连接了。

还有一次运维误操作,把Redis容器删了,结果应用连接池全部超时,线上故障30分钟。复盘时发现,我们根本没有服务依赖关系和健康检查的约束,全靠人肉运维。

这就是典型的容器化部署缺乏编排管理的问题。Docker Compose不是万能的,但处理单机多容器编排,它是最轻量、最实用的工具。下面直接进入正题。

二、环境与版本

先说明我的运行环境,避免版本差异导致配置不兼容:

Docker Engine: 24.0.7
Docker Compose: v2.24.2
操作系统: Ubuntu 22.04 LTS

这个项目用的镜像版本:

  • nginx:1.25.3-alpine(反向代理,体积只有23MB)
  • openjdk:17-jdk-slim(Spring Boot 3.1.5应用)
  • postgres:15.4-alpine(业务数据库,含WAL归档)
  • redis:7.2.3-alpine(缓存,关闭持久化)

三、方案设计

整体架构是标准的四层:Nginx作为入口,转发到Spring Boot应用,应用连接PostgreSQL和Redis。编排重点解决三个问题:

  1. 网络隔离:创建两个网络——frontend(Nginx→应用)和backend(应用→DB/Redis),避免服务直接暴露,同时也防止Redis和PostgreSQL被外部访问。

  2. 健康检查:每个服务定义healthcheck,用实际协议探测(TCP/HTTP)而不是简单的pg_isreadyredis-cli ping。这样depends_on才能拿到真实状态。

  3. 启动顺序depends_on配合condition: service_healthy,确保PostgreSQL和Redis完全就绪后,Spring Boot应用才启动。

四、核心实现(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:
  postgres_data:
    driver: local
  nginx_logs:
    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/ssl:/etc/nginx/ssl:ro
      - nginx_logs:/var/log/nginx
    networks:
      - frontend
    depends_on:
      app:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 5s
    restart: unless-stopped

  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    image: myapp:1.2.0
    container_name: spring-app
    expose:
      - "8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: business_db
      REDIS_HOST: redis
      REDIS_PORT: 6379
      JAVA_OPTS: "-Xms512m -Xmx1g -XX:+UseG1GC"
    networks:
      - frontend
      - backend
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 40s
    restart: unless-stopped

  postgres:
    image: postgres:15.4-alpine
    container_name: postgres-db
    environment:
      POSTGRES_USER: biz_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: business_db
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d:ro
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U biz_user -d business_db"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 2g
          cpus: '1.5'

  redis:
    image: redis:7.2.3-alpine
    container_name: cache-redis
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 5s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: '0.5'

这里有几个细节:

  • app服务同时挂两个网络,它既能被Nginx通过frontend访问,又能通过backend连数据库和缓存。
  • postgres的数据卷是命名卷postgres_data,删容器不丢数据。init-scripts目录只读挂载,用于初始化表结构。
  • 所有密码用环境变量注入,.env文件管理,不硬编码在yml里。

五、启动顺序验证与踩坑

我踩过最大的坑是:depends_on不加condition等于没用。Compose v2默认depends_on只控制容器创建顺序,不等待服务就绪。如果只写:

depends_on:
  - postgres
  - redis

应用容器会和数据库同时启动,大概率连接失败。必须显式声明:

condition: service_healthy

还有一个坑是健康检查命令的兼容性openjdk:17-jdk-slim镜像里没有curl,我一开始用curl做健康检查,容器直接报错。后来改用wget或者用Java进程检测:

# 不依赖外部工具的健康检查
test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/8080'"]

这个技巧来自Stack Overflow,实测可用,但更推荐在应用镜像里装curl,一劳永逸。

启动顺序验证方法,看日志时间戳:

$ docker compose up -d
[+] Running 5/5
✔ Container postgres-db  Healthy
✔ Container cache-redis  Healthy
✔ Container spring-app   Started (waiting for health...)
✔ Container gateway-nginx Started

注意postgres-dbcache-redis先变成Healthy,然后spring-app才启动。如果应用健康检查失败,nginx不会启动,不会出现流量打到坏服务的情况。

六、效果数据与优化

改造前后对比:

指标 改造前 改造后
部署时间(含启动等待) 约8分钟 约1分30秒
启动失败率(应用连不上DB) 约30% 0%
服务可用性(月度) 92% 99.5%
滚动回滚时间 15分钟(手动) 2分钟(compose down + up)

性能方面的优化:

  1. JVM参数调优-Xms512m -Xmx1g,堆内存固定下来,避免动态扩容导致的GC停顿。实测接口响应P95从180ms降到120ms。

  2. Redis内存策略allkeys-lrumaxmemory 256mb,防止缓存数据膨胀把容器内存打满。

  3. PostgreSQL数据卷:用命名卷而不是bind mount,IO性能提升约15%(实测pgbench TPS从3200到3700)。

  4. 日志轮转:Nginx日志用命名卷nginx_logs,配合logrotate定期清理,避免磁盘写满。

七、总结

Docker Compose编排对于单机多容器部署,性价比极高。核心要点就三条:

  1. 网络按需隔离,不要把所有服务丢到一个默认网络,用frontendbackend划分边界。
  2. 健康检查是编排的灵魂healthcheck + depends_on.condition才能实现真正的顺序控制。
  3. 资源限制必须写deploy.resources防止某个容器把宿主机资源吃光拖垮其他服务。

这套配置已经在生产环境稳定运行两个月,中间经历了一次Redis宕机和一次PostgreSQL重启,应用都自动恢复了,没有人工介入。

最后提醒:Compose适合单机场景,如果是多节点集群,建议上Kubernetes或Docker Swarm。但单机场景下,Compose是最快最稳的方案,没有之一。