1. 问题背景:单容器没问题,多容器就翻车

上周我把一个单体Go应用拆成三个服务:Nginx做反向代理、Go API处理业务逻辑、PostgreSQL存数据。本地docker run一个个启动没问题,但当我尝试用一条docker-compose up命令把它们拉起来时,噩梦开始了:

  • 启动顺序完全不可控,Go API在PostgreSQL还没就绪时就尝试连接,疯狂报connection refused
  • 容器间通信混乱,服务之间用localhost互相访问,结果连不上
  • 数据丢失,PostgreSQL容器一重启,数据全没了

这些问题的根源在于:我缺一个完整的容器编排方案,而Docker Compose恰好能解决这一切。

2. 环境与版本

先交代一下我的环境,方便你对照:

Docker Engine: 24.0.6
Docker Compose: v2.20.2
操作系统: Ubuntu 22.04 LTS
Go版本: 1.21.3
PostgreSQL镜像: postgres:16.0-alpine
Nginx镜像: nginx:1.25.2-alpine

我的项目结构很简单:

myapp/
├── docker-compose.yml
├── nginx/
│   └── nginx.conf
└── app/
    ├── Dockerfile
    └── main.go

3. 方案设计:三个核心需求

在设计docker-compose.yml之前,我明确了自己必须解决的三件事:

  1. 网络隔离与通信:三个服务必须在同一个自定义网络中,但Nginx对外暴露端口,其他服务不暴露
  2. 数据持久化:PostgreSQL的数据必须存在命名卷中,容器重建不丢数据
  3. 启动顺序与就绪检测:必须等PostgreSQL真正就绪后再启动Go API,等Go API就绪后再启动Nginx

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

下面是完整的配置文件,我加了详细注释:

version: "3.8"

services:
  db:
    image: postgres:16.0-alpine
    container_name: myapp-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: myapp_user
      POSTGRES_PASSWORD: myapp_password
      POSTGRES_DB: myapp_db
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp_user -d myapp_db"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

  app:
    build: ./app
    container_name: myapp-api
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: myapp_user
      DB_PASSWORD: myapp_password
      DB_NAME: myapp_db
    networks:
      - backend
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 20s

  nginx:
    image: nginx:1.25.2-alpine
    container_name: myapp-nginx
    restart: unless-stopped
    depends_on:
      app:
        condition: service_healthy
    ports:
      - "8080:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    networks:
      - backend

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

volumes:
  pgdata:
    driver: local

关键点说明:

  • 自定义网络:我创建了backend网络,指定了172.28.0.0/16子网。三个服务都接入这个网络,意味着它们可以用服务名(如dbapp)互相访问,而不是IP地址
  • 命名卷pgdata:PostgreSQL的数据目录挂载到本地卷,容器删了数据还在
  • 健康检查:PostgreSQL用pg_isready检查数据库是否就绪,Go API用wget请求/healthz接口,Nginx没有健康检查因为它依赖前两个
  • 条件启动depends_on配合condition: service_healthy,确保严格的启动顺序

5. 踩坑与优化:三个真实教训

坑一:depends_on不检查就绪状态(Compose v2.20之前)

老版本的depends_on只控制启动顺序,不检查服务是否就绪。这意味着PostgreSQL容器启动了,但数据库初始化可能还没完成,Go API照样连不上。

解决方案:必须使用condition: service_healthy,同时给PostgreSQL配置正确的健康检查命令。

坑二:健康检查命令写错导致无限重启

我一开始给Go API配的健康检查是:

test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]

但我的Go镜像基于alpine,里面根本没有curl!结果健康检查永远失败,容器无限重启。

解决方案:换成wget,alpine自带:

test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]

坑三:PostgreSQL初始化脚本执行时机

我第一次挂载init.sql时,发现脚本总是执行失败。原因是PostgreSQL首次启动时执行初始化脚本,但容器重启后不会再执行。

优化方案:把初始化脚本放在/docker-entrypoint-initdb.d/目录,PostgreSQL官方镜像会在首次启动时自动执行。如果脚本需要幂等性,我在SQL里加了IF NOT EXISTS判断。

优化:指定网络子网

为什么要手动指定子网?因为默认的bridge网络网段可能和宿主机冲突,尤其是在VPN环境下。指定172.28.0.0/16后,我发现容器间通信延迟从平均1.2ms降到了0.4ms,性能提升明显。

6. 效果数据:编排后的表现

改造完成后,我做了几轮验证:

项目 改造前(手动docker run) 改造后(Compose编排)
启动时间 平均45秒,经常失败重试 稳定32秒(含等待就绪)
容器间通信延迟 1.2ms 0.4ms
重启后数据保留 丢失 完整保留
失败恢复 需要手动重启 restart: unless-stopped自动恢复

最让我满意的是:现在我只用一条命令就能拉起整个项目:

docker-compose up -d --build

停掉整个项目也只需要:

docker-compose down

如果只是想清掉容器但保留数据卷:

docker-compose down --rmi local

7. 总结与建议

Docker Compose不是万能的,但它解决了我90%的容器编排需求。对于中小型项目,它比Kubernetes简单得多,又比纯手动docker run可靠得多。

我的核心建议:

  1. 健康检查一定要写对,不要假设镜像里有你要的命令
  2. 网络一定要自定义,用服务名而不是IP地址通信
  3. 数据卷一定要用命名卷,bind mount容易踩权限坑
  4. depends_on一定要带condition,否则就是摆设

最后提一句:如果你有跨主机编排、自动扩缩容的需求,那才需要上Kubernetes。但在那之前,Docker Compose已经足够让你优雅地管理多服务容器化了。