一、问题背景:手动部署的痛点与Compose的引入

上个月接手一个内部工单系统,架构很简单:Nginx作反向代理 + Go Gin API + PostgreSQL。但每次部署都在重复劳动:

  • 要先跑PostgreSQL,等它初始化完(大约10秒)
  • 再跑Go API,它启动时需要连数据库建表(失败就panic重启)
  • 最后跑Nginx,配置里upstream指向API容器名

我写了个shell脚本,里面docker run命令堆了7-8个参数(端口映射、卷挂载、网络连接),还用了sleep硬等。后来一次服务器重启,脚本跑完发现Nginx报502,因为API启动时数据库还没就绪。改成docker-compose后,用healthcheck + condition: service_healthy彻底解决了顺序依赖问题。

二、环境与版本

  • Docker Engine:24.0.7(Linux amd64)
  • Docker Compose:v2.24.2(通过插件安装,非独立二进制)
  • 镜像版本:
  • nginx:1.25.3-alpine(alpine版体积小,仅23MB)
  • golang:1.22.2-alpine(构建用)
  • postgres:16.2(官方镜像,内置健康检查支持)
  • 宿主机:Ubuntu 22.04 LTS,内核5.15.0

注意:Docker Compose v2已内置在Docker CLI中,推荐使用docker compose(无横杠)而非旧版docker-compose。新版对healthcheck和depends_on的支持更稳定。

三、方案设计:三服务拓扑

整个系统分为三层:

用户请求 → [Nginx:80] → [Go API:8080] → [PostgreSQL:5432]
                     (upstream指向api容器名)   (数据库名:workflow)

网络设计:
- 使用自定义bridge网络app-net,容器间通过服务名通信,避免IP硬编码
- 不暴露数据库端口到宿主机(安全性考虑),仅API和Nginx暴露

存储设计:
- PostgreSQL数据挂载命名卷pgdata,保证容器重建不丢数据
- Go API的配置文件通过bind mount挂载,方便修改后热加载

健康检查策略:
- PostgreSQL:使用内置pg_isready命令,每5秒检查一次,连续3次失败则标记为unhealthy
- Go API:自定义HTTP健康端点/health,返回200且body包含"status":"ok"才视为成功
- Nginx:依赖API健康状态,无独立健康检查(Nginx对上游健康检查由自身proxy_pass处理)

四、核心实现:docker-compose.yml详解

以下为生产级配置,包含网络、卷、健康检查和启动顺序控制。代码可直接复制使用(需调整镜像名和路径)。

version: "3.8"

services:
  postgres:
    image: postgres:16.2
    container_name: workflow-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: workflow
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret123}
      POSTGRES_DB: workflow
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d:ro
    networks:
      - app-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U workflow -d workflow"]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 15s  # 首次健康检查等待时间,避免镜像刚启动就检查失败
    ports:
      # 非生产建议不暴露,此处仅用于本地调试
      - "5432:5432"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
      args:
        GOPROXY: https://goproxy.cn,direct
    container_name: workflow-api
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy  # 关键:等待PostgreSQL健康检查通过
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: workflow
      DB_PASSWORD: ${DB_PASSWORD:-secret123}
      DB_NAME: workflow
      GIN_MODE: release
    volumes:
      - ./api/config:/app/config:ro  # 配置文件只读挂载
      - api-logs:/app/logs
    networks:
      - app-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 20s
    ports:
      - "8080:8080"

  nginx:
    image: nginx:1.25.3-alpine
    container_name: workflow-nginx
    restart: unless-stopped
    depends_on:
      api:
        condition: service_healthy  # 确保API就绪后再启动Nginx
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/html:/usr/share/nginx/html:ro
      - nginx-logs:/var/log/nginx
    networks:
      - app-net
    ports:
      - "80:80"
      - "443:443"  # HTTPS预留
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "2"

volumes:
  pgdata:
    driver: local
  api-logs:
    driver: local
  nginx-logs:
    driver: local

networks:
  app-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
          gateway: 172.20.0.1

几个关键细节:

  1. condition: service_healthy:这是Docker Compose v2.4+支持的特性,比旧的depends_on(只保证容器启动,不保证服务可用)强得多。实测中,即使PostgreSQL容器已启动,但还没完成初始化和WAL恢复(大约5-8秒),API启动依然会连接失败。加了健康检查后,API会在PostgreSQL健康通过后才启动,彻底解决了“数据库未就绪”问题。

  2. start_period:PostgreSQL的start_period: 15s很关键。首次启动时,容器需要初始化数据目录、运行初始化脚本,此时pg_isready会返回错误。设置15秒的启动宽限期,期间健康检查失败不计入重试次数。实际测试,初始化3个SQL脚本(建表+种子数据)需8秒,15秒足够。

  3. 环境变量提取:数据库密码通过${DB_PASSWORD:-secret123}读取宿主环境变量,如果未设置则使用默认值。生产环境推荐在.env文件中定义,并加入.gitignore

五、踩坑与优化

坑1:Nginx upstrem解析问题
一开始Nginx配置里写proxy_pass http://api:8080,但启动发现502。排查发现Nginx在启动时解析upstream域名,而DNS缓存一旦过期,如果API容器重启(IP变化),Nginx不会重新解析。
解决:在Nginx配置中加入resolver 127.0.0.11 valid=10s;(Docker内置DNS的IP是127.0.0.11),并使用变量方式:

location /api/ {
    resolver 127.0.0.11 valid=10s;
    set $upstream http://api:8080;
    proxy_pass $upstream;
}

实测这样API容器重启后Nginx能自动感知,502概率从30%降至0%。

坑2:日志卷权限问题
Go API通过挂载的api-logs卷写日志,容器内进程以user:1000运行,但卷目录在宿主机上属于root,导致权限拒绝。
解决:在Dockerfile中创建日志目录并赋予权限:

RUN mkdir -p /app/logs && chown -R 1000:1000 /app/logs

或者在Compose中指定用户运行:user: "1000:1000"

坑3:健康检查命令兼容性
Alpine镜像默认不包含curl,所以API健康检查必须用curl命令时,需要在Dockerfile中安装:

RUN apk add --no-cache curl

或者改用wget -qO-。PostgreSQL官方镜像已内置pg_isready,无需额外安装。

六、效果数据

在开发机(4核8G,SSD)上测试:

  • 启动速度:从执行docker compose up -d到三个服务全部健康,平均耗时14.7秒(10次测试)。其中PostgreSQL初始化占8秒,API启动占2秒,Nginx几乎瞬时。
  • 重启恢复:手动docker compose restart api,Nginx能自动感知新IP,整个切换耗时约3秒(包括API健康检查恢复)。
  • 资源占用:三服务合计内存占用约185MB(PostgreSQL 120MB,API 45MB,Nginx 20MB),CPU空闲时几乎为0。
  • 稳定性:连续运行72小时,无一次502或连接失败。日志无异常报错。

相比之前手动脚本方案,部署时间缩短了88%,错误率从20%降至0。

七、总结

Docker Compose的核心价值不在于“一键启动”,而在于通过声明式配置管理服务间的依赖、网络和存储。这次实践让我深刻理解了几点:

  1. 健康检查+depends_on condition是解决启动顺序问题的终极方案,远比sleep或wait-for-it脚本可靠。
  2. 自定义网络 + 服务名通信是容器化应用的标配,能让你无痛进行服务拆分和扩容。
  3. 日志卷和配置文件的挂载策略需要结合容器内用户权限设计,否则上线后就是血泪教训。

这个配置已经在我们团队内部作为模板推广,适用于中小型Web服务的容器化部署。如果你也在用Compose编排多服务,建议从这份配置开始修改,至少能避开我踩过的90%的坑。