一、问题背景:当容器启动顺序失控
最近在接手一个遗留项目时,遇到一个典型的容器化部署痛点:四个服务(前端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_on的condition: 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_data和redis_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. 健康检查的interval和retries不要设太短,否则数据库初始化慢时会误报;也不要太长,拖慢启动链。我推荐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管理敏感信息。