一、问题背景:从单容器到多服务编排的阵痛
上个月接手一个内部工具平台,架构是标准的“前端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
二、方案设计:网络、卷、健康检查三位一体
编排设计核心就三件事:网络让服务互相发现,卷让数据持久化,健康检查让依赖关系可控。
我的设计思路:
- 自定义网络:创建一个backend网络,所有服务挂进去,用服务名代替IP互访
- 命名卷:数据库和缓存数据挂到命名卷,容器删了数据还在
- 健康检查:PostgreSQL用
pg_isready探活,Redis用redis-cli ping探活,Golang服务用HTTP/healthz接口探活 - 启动顺序:
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”,而是用声明式配置把服务间的依赖关系、数据持久化、健康状态管理统一起来。几个经验之谈:
- depends_on一定要配condition,否则等于没写
- 数据卷必须用命名卷,bind mount适合配置文件,不适合数据库文件
- healthcheck的start_period要按服务初始化时间调,太小会误判,太大浪费时间
- 网络用自定义桥接网络,别用默认的bridge,服务间解析更可控
如果服务超过5个,或者有动态扩缩容需求,可以考虑上K8s或Docker Swarm,但中小型项目docker-compose完全够用,别过度设计。
有问题的欢迎在评论区交流,特别是healthcheck的写法,不同镜像基础不同,踩坑姿势也各不相同。