一、问题背景:多服务编排的三大痛点
上个月接手一个遗留项目,4个服务(Nginx前端、Java后端、PostgreSQL、Redis)靠一堆shell脚本启动。每次部署都要手动敲命令,顺序错了就报连接拒绝。更头疼的是:
- 启动顺序不可控:后端还没等数据库就绪就启动,连接池疯狂重试,日志刷屏
- 网络混乱:所有容器都在默认bridge网络,IP地址随机分配,配置里写死IP,一重启就失效
- 数据丢失:PostgreSQL数据存在容器可写层,
docker rm后数据全没了
这显然是Docker Compose的典型应用场景。但网上教程大多只讲基础用法,真正生产级配置需要解决网络隔离、数据持久化、健康检查、启动依赖这些实际问题。
二、环境与版本
先交代一下我的环境,避免版本差异导致配置不兼容:
- Docker Engine:24.0.7
- Docker Compose:v2.24.2(注意,v1和v2的语法略有差异,本文使用v2)
- 宿主机:Ubuntu 22.04 LTS,4核8G
- 镜像版本:nginx:1.25.3-alpine、postgres:16.1-alpine、redis:7.2.3-alpine、后端镜像(自建,基于openjdk:17-jdk-alpine)
三、方案设计:三层架构的Compose编排
我的设计思路是:
- 网络:创建两个自定义网络。
frontend网络只挂Nginx和后端,backend网络挂后端、PostgreSQL、Redis。这样前端无法直达数据库,安全组规则更清晰 - 卷:PostgreSQL数据挂到命名卷
pg_data,Redis持久化挂到redis_data。后端日志挂到./logs目录,方便宿主机直接查看 - 健康检查:三个依赖服务(PostgreSQL、Redis、后端)都配置
healthcheck。后端启动前依赖PostgreSQL和Redis健康,Nginx依赖后端健康 - 启动顺序:利用
depends_on的condition: service_healthy,确保严格按依赖链启动
这个设计的核心思想是:让每个服务只暴露必要的通信路径,数据不丢失,启动不慌。
四、核心实现:完整docker-compose.yml
直接上完整配置,每个服务都附关键注释:
version: '3.8'
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
backend:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/24
volumes:
pg_data:
driver: local
redis_data:
driver: local
services:
# ---------- 前端入口 ----------
nginx:
image: nginx:1.25.3-alpine
container_name: gateway-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/certs:/etc/nginx/certs:ro
networks:
- frontend
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
# ---------- 后端API ----------
backend:
image: myapp/backend:1.4.2
container_name: app-backend
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: myapp
DB_USER: myapp_user
DB_PASSWORD: ${DB_PASSWORD} # 从.env文件读取
REDIS_HOST: redis
REDIS_PORT: 6379
volumes:
- ./logs:/app/logs
networks:
- frontend
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
# ---------- 数据库 ----------
postgres:
image: postgres:16.1-alpine
container_name: db-postgres
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
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 myapp_user -d myapp"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
restart: unless-stopped
# ---------- 缓存 ----------
redis:
image: redis:7.2.3-alpine
container_name: cache-redis
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
restart: unless-stopped
注意几个细节:
container_name:固定容器名,方便日志排查和运维脚本${DB_PASSWORD}:从.env文件读取敏感信息,不要硬编码在YAML里restart: unless-stopped:崩溃自动重启,但手动停止不拉起start_period:给服务预热时间,避免启动慢被误判为不健康
五、踩坑与优化:三个真实问题
坑1:健康检查命令找不到
后端容器基于openjdk:17-jdk-alpine,里面没有curl。第一次配置test: ["CMD", "curl", "-f", "http://localhost:8080/health"]直接报exec: "curl": executable file not found。
解决:换用wget(alpine自带)或者改用Java的/actuator/health探测。我最终在Dockerfile里加了RUN apk add --no-cache curl,一劳永逸。
坑2:depends_on只等容器启动,不等服务就绪
depends_on默认只检查容器是否创建成功,不关心服务是否就绪。早期配置depends_on: [postgres, redis],后端启动后连数据库失败。
解决:必须配合condition: service_healthy,让Compose等待健康检查通过才启动依赖方。
坑3:PostgreSQL数据初始化脚本重复执行
我把建表SQL放在./init-sql目录,首次启动执行成功。但后来改了SQL,重启容器发现没执行。
原因:/docker-entrypoint-initdb.d只在数据目录为空时执行。
解决:需要执行docker compose down -v清卷重来,或者手动psql导入。这个坑提醒我生产环境禁止用init-sql做schema变更,应该走Flyway/Liquibase。
优化:启动耗时压测
优化前:纯shell脚本启动,无健康检查,平均需要48秒(后端重试3次才连上DB)。
优化后:用healthcheck + depends_on组合,平均22秒完成全部服务拉起。其中PostgreSQL就绪花了8秒,Redis花了2秒,后端依赖检查通过后12秒完成启动。
六、效果数据:从99.2%到99.9%
部署这套Compose配置后,做了7天连续压测(QPS 2000并发):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均启动时间 | 48s | 22s |
| 服务可用性 | 99.2% | 99.9% |
| 数据丢失事故 | 2次 | 0次 |
| 部署失败率 | 15% | 0.5% |
最明显的改善是部署失败率从15%降到0.5%,以前经常因为启动顺序问题导致后端连不上DB,现在完全是自动化的。
七、总结与建议
这套配置已经稳定运行两个月,总结几点经验:
- 自定义网络必须做,哪怕只有两个服务。默认bridge网络无法控制IP,安全性和可维护性都很差
- 命名卷是数据安全的底线,尤其是数据库。别图省事用bind mount到宿主机目录,权限问题会烦死你
- 健康检查要针对业务设计,
pg_isready、redis-cli ping、curl /actuator/health都是标准做法。不要用CMD-SHELL去ping容器IP,那只能证明网络通,不能证明服务可用 .env文件管理密钥,不要提交到Git仓库。docker compose config可以验证变量是否解析正确
如果你也在做微服务多容器部署,建议直接复制这份配置改改服务名和镜像版本。遇到问题可以评论区交流。
附:常用命令速查
# 启动所有服务
docker compose up -d
# 查看服务状态(显示健康检查结果)
docker compose ps
# 查看日志(实时跟踪)
docker compose logs -f backend
# 重新构建并启动(代码变更后)
docker compose up -d --build
# 停止并删除容器(保留卷)
docker compose down
# 停止并删除容器+卷(危险,数据全没)
docker compose down -v