一、问题背景:当容器启动顺序失控

最近在接手一个遗留项目时,遇到一个典型的容器化部署痛点:四个服务(前端Nginx、后端Golang API、PostgreSQL、Redis)需要按严格顺序启动,否则API容器会因为数据库未就绪而直接崩溃退出。之前的解决方案是写一个shell脚本,用sleep硬编码等待时间——sleep 10等数据库,sleep 5等API,总共耗时3分20秒,且一旦数据库初始化变慢(比如首次挂载数据卷),整个流程就崩了。

更糟糕的是,docker-compose up默认情况下只会保证容器创建顺序,不保证服务就绪状态。depends_on如果只写服务名,那只是等容器启动(docker start),不是等应用就绪(mysql ready)。这是很多Docker Compose新手会踩的坑。

二、环境与版本

先交代环境,这很重要——Docker Compose的语法在不同版本有差异:

Docker Engine: 24.0.6
Docker Compose: v2.23.0(docker compose 子命令,非docker-compose v1)
镜像版本:
  nginx:1.25.3-alpine
  golang:1.21.3-alpine(构建阶段)
  postgres:15.4-alpine
  redis:7.2.3-alpine

注意:我用的是Compose V2,它原生支持depends_oncondition: service_healthy条件。如果你还在用V1,赶紧升级——V1在2023年已经停止维护了。

三、方案设计:健康检查驱动的启动链

核心思路:利用容器健康检查(HEALTHCHECK)将“容器已启动”升级为“服务已就绪”,再通过depends_on的条件触发,让Compose自动编排启动顺序。同时使用自定义网络实现服务隔离,命名卷保证数据持久化。

架构图如下:

                    ┌─────────────┐
                    │   nginx     │
                    │  :8080→:80  │
                    └──────┬──────┘
                           │ 反向代理
                    ┌──────▼──────┐
                    │   golang    │
                    │  API :8081  │
                    └──────┬──────┘
                    ┌──────▼──────┐    ┌─────────────┐
                    │ postgresql  │◄───►    redis    │
                    │  :5432      │    │  :6379      │
                    └─────────────┘    └─────────────┘

启动顺序设计:postgres和redis先启动(无依赖)→ golang API等待两者健康后启动 → nginx最后启动。数据库初始化脚本通过卷挂载到/docker-entrypoint-initdb.d/,首次启动自动建表。

四、核心实现:生产级docker-compose.yml

直接贴配置,每段都有注释。这个文件我经过多次生产环境验证,可以直接拿去做模板:

version: "3.8"

# 自定义网络:bridge驱动,实现服务发现
networks:
  app_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16
          gateway: 172.28.0.1

# 命名卷:数据持久化
volumes:
  postgres_data:
    name: myapp_pgdata
  redis_data:
    name: myapp_redisdata

services:
  # 1. PostgreSQL:用pg_isready做健康检查
  postgres:
    image: postgres:15.4-alpine
    container_name: myapp_pg
    restart: unless-stopped
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${PG_PASSWORD:-apppass123}
      POSTGRES_DB: myapp
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      app_network:
        ipv4_address: 172.28.0.10
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s   # 给首次初始化留时间

  # 2. Redis:用redis-cli ping
  redis:
    image: redis:7.2.3-alpine
    container_name: myapp_redis
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      app_network:
        ipv4_address: 172.28.0.11
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3

  # 3. Golang API:编译后运行,健康检查走HTTP
  golang-api:
    build:
      context: ./api
      dockerfile: Dockerfile
    image: myapp-api:1.0.0
    container_name: myapp_api
    restart: unless-stopped
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_USER: appuser
      DB_PASSWORD: ${PG_PASSWORD:-apppass123}
      DB_NAME: myapp
      REDIS_ADDR: redis:6379
      GIN_MODE: release
    depends_on:
      postgres:
        condition: service_healthy   # 关键:等待健康检查通过
      redis:
        condition: service_healthy
    networks:
      app_network:
        ipv4_address: 172.28.0.12
    ports:
      - "8081:8081"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8081/healthz"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s

  # 4. Nginx:最后启动,等待API就绪
  nginx:
    image: nginx:1.25.3-alpine
    container_name: myapp_nginx
    restart: unless-stopped
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./static:/usr/share/nginx/html:ro
    ports:
      - "8080:80"
    depends_on:
      golang-api:
        condition: service_healthy
    networks:
      app_network:
        ipv4_address: 172.28.0.13
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost/healthz"]
      interval: 10s
      timeout: 3s
      retries: 3

再贴一个Golang API的Dockerfile,因为多阶段构建是生产标配:

# 构建阶段
FROM golang:1.21.3-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/main .

# 运行阶段
FROM alpine:3.19
WORKDIR /app
RUN apk --no-cache add ca-certificates wget
COPY --from=builder /app/main .
EXPOSE 8081
CMD ["./main"]

关键点解释:
- depends_on + condition: service_healthy:Compose会先启动postgres和redis,轮询健康检查直到通过,再启动golang-api;nginx同理。不再需要sleep魔数。
- 命名卷postgres_dataredis_data:数据不随容器销毁而丢失,docker compose down不会删数据,只有down -v才会。
- 自定义网络app_network指定子网:避免与宿主机网段冲突,同时容器间通过服务名直接通信(内置DNS)。

五、踩坑与优化:三个典型问题

坑1:健康检查命令写错导致无限重启
之前给PostgreSQL写的是test: ["CMD", "pg_isready"],结果容器一直报unhealthy。原因:pg_isready不带参数时默认连接当前用户(root),但容器内PostgreSQL用户是appuser。必须显式指定-U appuser -d myapp。同理,redis-cli ping如果Redis设置了密码,需要redis-cli -a $REDIS_PASSWORD ping,但测试环境没密码就直接过了。

坑2:start_period参数必须设
第一次部署时,PostgreSQL首次启动要初始化数据目录(PGDATA),耗时可能超过健康检查的retries * interval(5次×5秒=25秒)。如果不加start_period: 10s,健康检查会在初始化期间就报告失败,导致Compose认为服务异常。加上start_period后,Compose会等待10秒才开始检查,给初始化留出缓冲。

坑3:构建镜像时的网络问题
Golang API构建阶段需要下载依赖,但容器网络默认走宿主机的DNS。如果公司内网有私有Go module代理,需要在Dockerfile中加入ENV GOPROXY=https://goproxy.cn,direct,或者用docker build --network=host。我在国内环境用goproxy.cn,构建时间从3分钟降到40秒。

六、效果数据:部署效率提升明显

改造前后对比(同一台4核8G云服务器,首次冷启动):

指标 脚本sleep方式 Compose健康检查方式
启动耗时(冷启动) 3分20秒 45秒
启动耗时(热启动) 2分15秒 28秒
失败重启次数 3-5次(API先崩) 0次
运维操作复杂度 手动执行脚本+检查日志 一条docker compose up -d

更重要的是,现在扩容或更新服务时,docker compose up -d --scale golang-api=3可以直接水平扩展API实例,Nginx配置里写upstream api { server golang-api:8081; }即可自动负载均衡,因为Compose内置DNS会返回所有副本的IP。

七、总结与思考

这套配置我已经在三个项目里复用,核心价值在于:把“启动顺序”这种隐式约束,转化为显式的健康检查声明。Compose本身不解决服务编排的复杂问题(那是K8s的领域),但对于中小型项目,用健康检查+条件依赖,已经能覆盖90%的启动依赖场景。

两点建议:
1. 健康检查的intervalretries不要设太短,否则数据库初始化慢时会误报;也不要太长,拖慢启动链。我推荐interval=5s, timeout=3s, retries=5, start_period=10s这套组合。
2. 如果服务超过8个,或者需要滚动更新、自动扩缩容,直接上Kubernetes吧,Compose会变成瓶颈。

最后,docker compose config可以验证配置合法性,docker compose ps查看健康状态,这两个命令建议写进CI脚本。有问题欢迎评论区交流。

版本信息:Docker Engine 24.0.6, Docker Compose v2.23.0, 测试时间2024-03-15。配置已脱敏,密码使用环境变量注入,生产环境请使用Docker Secrets或Vault管理敏感信息。