一、为什么需要编排?—— 手动部署的崩溃瞬间
上周五下午,我把一个包含4个微服务的项目部署到测试环境。按照老办法:先装PostgreSQL,再启动Redis,接着跑Spring Boot jar包,最后配Nginx反向代理。结果呢?数据库还没初始化完成,应用已经连不上库;Redis缓存没预热,接口响应超时;Nginx upstream配置写错一个端口,整个网关504。花了40分钟排查,最后发现是启动顺序和网络配置的锅。
这不是偶然。当服务数量超过3个,手动部署的复杂度呈指数级上升。尤其是启动顺序——现代应用启动时依赖数据库schema、缓存预热、配置中心,任何一个环节没就绪,应用就会启动失败或疯狂重试。Docker Compose的depends_on条件、健康检查、自定义网络,正是解决这类问题的标准方案。
二、环境与版本 —— 别用老版本踩坑
先交代我的环境:
- Docker Engine: 24.0.7(已开启BuildKit)
- Docker Compose: v2.24.2(独立二进制,非docker-compose v1)
- 宿主机: Ubuntu 22.04 LTS, 8核16G
- 项目技术栈: Spring Boot 3.2.1 + PostgreSQL 15.3 + Redis 7.0.12 + Nginx 1.25.3
注意:Docker Compose v2和v1的语法有差异,v2支持depends_on.condition: service_healthy,v1不支持。如果你还在用v1,建议立刻迁移。
三、方案设计 —— 网络分段、卷挂载、健康检查三位一体
我们的业务场景是:一个电商后台系统,包含用户服务(Spring Boot)、订单服务(Spring Boot)、商品服务(Spring Boot),共享一个PostgreSQL和Redis。Nginx作为统一入口做负载均衡。
设计思路:
-
网络分段:创建两个bridge网络——
frontend_net和backend_net。Nginx只接入frontend_net,业务服务同时接入两个网络,数据库和Redis只接入backend_net。这样前端无法直接访问数据库,安全隔离。 -
卷挂载:PostgreSQL数据目录挂载到宿主机
/data/postgres,Redis持久化文件挂载到/data/redis。容器删了数据还在,升级不丢数据。 -
健康检查:每个服务配置
healthcheck。数据库用pg_isready命令,Redis用redis-cli ping,Spring Boot用curl /actuator/health。健康检查间隔30秒,超时10秒,最多重试5次。 -
启动顺序:用
depends_on的condition: service_healthy确保数据库和Redis先就绪,再启动业务服务。Nginx最后启动,保证upstream后端全部健康。
四、核心实现 —— 完整docker-compose.yml解析
先看完整配置文件(已脱敏),然后逐段拆解:
version: "3.8"
networks:
frontend_net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
backend_net:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/24
volumes:
postgres_data:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres
redis_data:
driver: local
driver_opts:
type: none
o: bind
device: /data/redis
services:
postgres:
image: postgres:15.3-alpine
container_name: ecommerce-postgres
restart: unless-stopped
networks:
- backend_net
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ecom_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ecommerce
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ecom_user -d ecommerce"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
redis:
image: redis:7.0.12-alpine
container_name: ecommerce-redis
restart: unless-stopped
networks:
- backend_net
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "$$REDIS_PASSWORD", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
user-service:
build: ./user-service
image: ecommerce/user-service:1.2.0
container_name: ecommerce-user
restart: unless-stopped
networks:
- frontend_net
- backend_net
environment:
SPRING_PROFILES_ACTIVE: docker
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ecommerce
DB_USER: ecom_user
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
nginx:
image: nginx:1.25.3-alpine
container_name: ecommerce-nginx
restart: unless-stopped
networks:
- frontend_net
ports:
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
user-service:
condition: service_healthy
1. 网络配置详解
frontend_net和backend_net分别占用不同的子网,避免IP冲突。Nginx只挂frontend_net,即使Nginx被攻破,也无法直接连通数据库。业务服务挂双网,通过服务名(postgres、redis)而非IP地址访问后端,Compose内置DNS解析。
2. 卷挂载的坑
这里用了bind mount方式绑定宿主机的/data/postgres和/data/redis。注意driver_opts里的type: none和o: bind,如果不写,Docker会创建volume而不是直接绑定目录。我用bind mount是因为要配合宿主机的备份任务(cron直接备份目录),如果你用云盘,建议用driver: local默认配置,数据存在/var/lib/docker/volumes/下。
3. 健康检查细节
PostgreSQL的pg_isready命令需要指定用户名和数据库名,否则可能误判。Redis的健康检查要注意密码注入——用$$REDIS_PASSWORD转义,避免被compose解析为环境变量。
Spring Boot的健康检查依赖spring-boot-starter-actuator,需要在application.yml里暴露health端点:
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
4. 启动顺序的完整闭环
depends_on的service_healthy条件确保:只有当PostgreSQL和Redis都健康后,user-service才会启动。start_period参数给了应用60秒的初始化时间,避免刚启动时的健康检查误报。
五、踩坑记录 —— 这三个坑浪费了我一个下午
坑1:depends_on不含restart语义
如果你在服务A中depends_on服务B,B崩溃后重启,A不会自动重启。需要在A上配置restart: unless-stopped,同时确保B的restart策略也是unless-stopped,否则B挂了A就永远等不到健康的B。
坑2:Redis密码在健康检查中的转义
redis-cli -a $$REDIS_PASSWORD ping,如果写成$REDIS_PASSWORD,Compose会把它当作宿主机环境变量展开,导致密码为空。$$才是转义后的字符串$REDIS_PASSWORD,在容器内解析。这个坑我排查了半小时,最后用docker inspect看容器环境变量才定位。
坑3:bind mount目录权限
如果宿主机/data/postgres的属主不是UID 999(PostgreSQL官方镜像的默认用户),容器启动会报权限错误。解决:chown -R 999:999 /data/postgres。Redis同理,UID是999。
六、效果数据 —— 对比手动部署
部署完这套编排后,我做了两组对比测试:
| 指标 | 手动部署 | Docker Compose |
|---|---|---|
| 冷启动时间(含镜像拉取) | 4分32秒 | 58秒(镜像已缓存) |
| 服务可用性(7天) | 99.2% | 99.95% |
| 故障恢复时间(数据库宕机) | 8分钟(手动排查) | 45秒(自动重启+健康检查) |
| 环境配置一致性 | 低(容易漏配) | 高(声明式配置) |
另外,docker compose config可以离线校验配置合法性,docker compose up -d可以一键启动,docker compose logs -f可以聚合查看所有服务日志。这些在手动部署时都需要额外写脚本。
七、总结与建议
Docker Compose不是万能的,它适合单机多容器编排。如果你的服务超过10个,或者需要跨主机部署,建议上Kubernetes。但在单机环境下,Compose配合健康检查和depends_on条件,已经能解决90%的编排痛点。
最后给三个建议:
- 所有服务必须配置
healthcheck,否则depends_on的service_healthy形同虚设。 - 敏感信息用
.env文件管理,不要硬编码在docker-compose.yml里。我上面的${DB_PASSWORD}就是读取.env文件。 - 生产环境加
logging配置,限制日志大小防止磁盘爆掉。比如:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
好了,这次的编排方案就分享到这里。如果你也遇到过启动顺序的坑,或者有更好的健康检查写法,欢迎在评论区讨论。