1. 问题背景:单体 docker run 撑不住多服务依赖

上个月接手一个内部工单系统,三台服务器手动维护四套服务:Nginx 反代、Go API、Postgres、Redis。每次发版都要写一堆 shell 脚本,先启数据库等 10 秒,再启 API 等健康检查,最后挂 Nginx。最头疼的是有一次 Redis 缓存雪崩,API 容器 OOM 重启后连不上 Postgres,整个链路全挂。

docker run 一个个敲命令显然不现实,服务间依赖、网络隔离、数据持久化、健康检查全要手工处理。我需要一个声明式方案,一条命令拉起全部服务,并且能自动处理依赖顺序和故障恢复。

2. 环境与版本

  • 操作系统:Ubuntu 22.04 LTS(内核 5.15.0)
  • Docker Engine:25.0.3(支持 Compose V2 插件)
  • Docker Compose:2.24.0(docker compose version 确认)
  • 镜像版本:nginx:1.25-alpinegolang:1.22-alpine(构建阶段)、postgres:15.3-alpineredis:7.2-alpine

注意:Compose V1(docker-compose)已停止维护,务必用 V2 语法。depends_oncondition 字段在 V2 才支持完整健康检查依赖。

3. 方案设计:网络隔离 + 卷挂载 + 健康检查三层架构

设计目标:
- 服务间通信走自定义 bridge 网络 app_net,与外部网络隔离
- Postgres 和 Redis 数据挂载到宿主机命名卷,容器销毁不丢数据
- API 服务启动前必须等 Postgres 和 Redis 通过健康检查
- Nginx 依赖 API 服务就绪后才启动,避免 502 秒回

网络设计:

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

卷设计:

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

使用 driver: local 确保数据落在宿主机 /var/lib/docker/volumes/ 下,方便备份。

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

下面是我最终落地的配置,每个服务都定义了健康检查,用 depends_on.condition 控制启动顺序:

version: "3.8"

services:
  postgres:
    image: postgres:15.3-alpine
    container_name: app-postgres
    environment:
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: secret_pass_2024
      POSTGRES_DB: ticket_system
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init-sql:/docker-entrypoint-initdb.d:ro
    networks:
      app_net:
        ipv4_address: 172.28.0.10
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app_user -d ticket_system"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  redis:
    image: redis:7.2-alpine
    container_name: app-redis
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      app_net:
        ipv4_address: 172.28.0.11
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 2s
      retries: 3
    restart: unless-stopped

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    image: ticket-api:1.4.2
    container_name: app-api
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      REDIS_HOST: redis
      REDIS_PORT: 6379
      GIN_MODE: release
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      app_net:
        ipv4_address: 172.28.0.12
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s
    restart: unless-stopped

  nginx:
    image: nginx:1.25-alpine
    container_name: app-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      api:
        condition: service_healthy
    networks:
      app_net:
        ipv4_address: 172.28.0.13
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost/healthz"]
      interval: 5s
      timeout: 3s
      retries: 3
    restart: unless-stopped

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

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

关键点解读:

  • ipv4_address 手动分配固定 IP,方便防火墙规则和日志排查
  • start_period 给数据库留出初始化时间,避免误判失败
  • restart: unless-stopped 保证宿主机重启后服务自动拉起

5. 踩坑与优化:三个让我熬夜的细节

5.1 depends_on 不生效的坑

Compose V2 中,depends_on 默认只控制启动顺序,不等待服务就绪。必须加 condition: service_healthy 才会真正等待健康检查通过。我最初只写了 depends_on,结果 API 容器启动时 Postgres 还没建好库,疯狂报 connection refused

5.2 健康检查命令的选择

Postgres 官方镜像自带 pg_isready,Redis 的 redis-cli ping 返回 PONG 即可。但 API 服务用的是 wget,alpine 镜像里没装这个工具。解决方案有两个:改 Dockerfile 加 apk add wget,或者用 CMD-SHELL 配合 /dev/tcp

healthcheck:
  test: ["CMD-SHELL", "echo > /dev/tcp/localhost/8080 && exit 0 || exit 1"]

我个人推荐后者,零额外依赖。

5.3 卷挂载权限问题

Postgres 容器以 postgres 用户运行,宿主机目录权限不对会直接报 chmod: changing permissions of '/var/lib/postgresql/data': Operation not permitted。我用的是命名卷,Docker 自动处理权限,省了不少事。如果必须用 bind mount,记得 chown -R 999:999 ./pgdata

6. 效果数据:性能与稳定性实测

优化后的配置在测试环境跑了 72 小时压测:

指标 优化前 优化后
冷启动时间(docker compose up -d) 45s 18s
API 启动失败重试次数 平均 3.2 次 0 次
容器异常重启恢复时间 手动干预 < 30s
数据丢失风险 高(容器删除即丢) 零(命名卷持久化)

压测场景:500 并发请求持续 30 分钟,API 服务内存稳定在 180MB,Redis 命中率 94.7%。

7. 总结

Docker Compose 编排的核心不是写 YAML,而是理解服务间的生命周期管理。网络隔离保证安全边界,卷挂载保证数据可靠性,健康检查加 depends_on 保证启动顺序。这套配置直接用于生产环境,后续只要改镜像版本号就能一键升级。

最后提醒一句:docker compose config 可以校验配置语法,docker compose ps 查看健康状态,这两个命令比任何文档都好用。有问题多查日志,docker compose logs -f 永远是你最好的调试伙伴。