一、问题背景:从单容器到多服务编排的阵痛

上个月接手一个内部工具平台,架构是标准的“前端Nginx静态资源 + 后端Golang API + PostgreSQL数据库 + Redis缓存”。刚开始图省事,每个服务单独docker run启动,结果踩了一连串坑:

  • 容器IP是动态的,Golang服务里写死数据库IP,重启后就失联
  • 每次部署要手动敲4条命令,顺序错了数据库还没起来API就挂了
  • 数据存在容器可写层,一docker rm数据全没
  • 排查问题要docker inspect挨个找IP,效率极低

后来决定统一用docker-compose编排。写这篇博客时,我用的环境是:

  • Docker Engine 24.0.7
  • Docker Compose v2.23.0
  • 云主机:2核4G CentOS 7.9

二、方案设计:网络、卷、健康检查三位一体

编排设计核心就三件事:网络让服务互相发现,卷让数据持久化,健康检查让依赖关系可控

我的设计思路:

  1. 自定义网络:创建一个backend网络,所有服务挂进去,用服务名代替IP互访
  2. 命名卷:数据库和缓存数据挂到命名卷,容器删了数据还在
  3. 健康检查:PostgreSQL用pg_isready探活,Redis用redis-cli ping探活,Golang服务用HTTP /healthz接口探活
  4. 启动顺序depends_on配合condition: service_healthy,确保数据库和缓存就绪后才启动API,API就绪后才启动Nginx

这里要吐槽一下:很多人只写depends_on不带condition,那玩意儿只控制启动顺序,不控制就绪状态,等于白写。Compose v2才开始支持condition: service_healthy,老项目迁移要注意版本。

三、核心实现:完整docker-compose.yml解析

直接上配置,这是生产环境精简后的版本,删掉了日志采集和监控相关部分:

version: "3.8"

services:
  postgres:
    image: postgres:15.4-alpine
    container_name: app-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ChangeMe_2024!
      POSTGRES_DB: appdb
      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 appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:7.2-alpine
    container_name: app-redis
    restart: unless-stopped
    command: redis-server --requirepass RedisPass_2024 --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "RedisPass_2024", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: app-api
    restart: unless-stopped
    environment:
      DB_HOST: postgres
      DB_PORT: "5432"
      DB_USER: appuser
      DB_PASSWORD: ChangeMe_2024!
      DB_NAME: appdb
      REDIS_ADDR: redis:6379
      REDIS_PASSWORD: RedisPass_2024
      GIN_MODE: release
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - backend
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s

  nginx:
    image: nginx:1.24-alpine
    container_name: app-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./frontend/dist:/usr/share/nginx/html:ro
    depends_on:
      api:
        condition: service_healthy
    networks:
      - backend
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost/healthz"]
      interval: 15s
      timeout: 5s
      retries: 3

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

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

四、踩坑与优化:三个让人抓狂的细节

坑1:healthcheck里的wget不存在

Golang的官方alpine镜像里没有wget,我healthcheck写wget -qO-直接报sh: wget: not found。解决方式有三种:

  • 改用CMD-SHELL配合curl,但alpine里也没有curl
  • 在Dockerfile里装busybox-extras提供wget
  • 最优雅的方案:用Golang官方提供的小工具,比如wget -qO-换成wget -qO-不行就换成Go程序自带的健康检查脚本

我最后在API的Dockerfile里加了:

FROM golang:1.21-alpine AS builder
# ... 构建阶段 ...

FROM alpine:3.18
RUN apk add --no-cache wget ca-certificates
COPY --from=builder /app/main /app/main
EXPOSE 8080
CMD ["/app/main"]

坑2:PostgreSQL健康检查的start_period

PostgreSQL首次初始化要20-30秒,如果start_period设短了,healthcheck会在初始化期间反复失败,导致depends_on等不到健康状态直接卡死。我调成start_period: 30s后,配合retries: 5,实测从docker compose up到API服务就绪,稳定在43秒左右。

坑3:Nginx反代Golang服务的超时设置

默认Nginx的proxy_read_timeout是60秒,但Golang服务里有个大报表接口要跑90秒。不想改业务代码,就在Nginx配置里单独给那个location设了超时:

location /api/report {
    proxy_pass http://api:8080;
    proxy_read_timeout 120s;
    proxy_connect_timeout 5s;
}

五、效果数据:从手动到编排的量化对比

改造前后对比:

指标 改造前(手动docker run) 改造后(docker-compose)
部署耗时 5-8分钟(含人工核对顺序) 43秒(全自动)
服务发现 手动查IP改配置 服务名直接解析
数据持久化 容器删除数据丢失 命名卷安全,实测重启10次无丢失
故障恢复 手动重启 restart: unless-stopped自动拉起
团队协作 每人一套配置,冲突不断 一份docker-compose.yml走天下

内存占用方面,四个容器合计稳定在512MB左右,其中PostgreSQL占180MB,Redis占80MB,Golang API占120MB,Nginx占30MB,剩余留给系统缓冲,2G内存的机器跑起来毫无压力。

六、总结与建议

Docker Compose编排的核心价值不是“把命令写进YAML”,而是用声明式配置把服务间的依赖关系、数据持久化、健康状态管理统一起来。几个经验之谈:

  1. depends_on一定要配condition,否则等于没写
  2. 数据卷必须用命名卷,bind mount适合配置文件,不适合数据库文件
  3. healthcheck的start_period要按服务初始化时间调,太小会误判,太大浪费时间
  4. 网络用自定义桥接网络,别用默认的bridge,服务间解析更可控

如果服务超过5个,或者有动态扩缩容需求,可以考虑上K8s或Docker Swarm,但中小型项目docker-compose完全够用,别过度设计。

有问题的欢迎在评论区交流,特别是healthcheck的写法,不同镜像基础不同,踩坑姿势也各不相同。