一、为什么需要精细编排?一个生产事故引发的思考
上个月我们团队在测试环境部署一套新开发的订单服务,docker-compose.yml 写得极其简单——只定义了服务、端口和镜像,没有任何健康检查和依赖控制。结果呢?Spring Boot 容器启动时 MySQL 还没就绪,Hibernate 建表语句全部执行失败,应用直接退出。我们用 restart: always 强行拉起来,但连接池已经缓存了错误的连接状态,导致后续请求大量超时。
更离谱的是,某次服务器重启后,MySQL 容器里的数据全丢了——因为当时用的匿名卷,容器重建后数据跟着没了。这两个事故直接促使我重新设计了整套编排方案。如果你也在用 Docker Compose 跑多服务项目,下面的配置建议直接抄作业。
二、环境与版本说明
先交代一下我的实际环境,避免版本差异导致配置不生效:
- Docker Engine:24.0.7(Linux 内核 5.15.0)
- Docker Compose:v2.23.3(注意:v1 的
version:字段已废弃,本文不写) - 宿主机:Ubuntu 22.04 LTS,4核8G
- 镜像版本:mysql:8.0.35、redis:7.2.3、openjdk:17-jdk-alpine、nginx:1.24.0
服务拓扑:Nginx 作为反向代理 → Spring Boot 应用(2个副本)→ MySQL + Redis。用 Nginx 做负载均衡和静态资源服务,应用容器通过内部网络访问数据库。
三、方案设计:网络、卷、健康检查三位一体
核心设计思路有四点:
- 自定义 bridge 网络:创建
app-network,让服务间通过容器名互访,避免暴露不必要的端口到宿主机。只有 Nginx 映射 80 端口。 - 命名卷持久化:MySQL 数据存
mysql-data卷,Redis 的 AOF 文件存redis-data卷,应用日志挂载宿主机目录。这样容器down再up数据不丢。 - healthcheck 健康检查:每个依赖服务都定义
healthcheck,Spring Boot 用depends_on的condition: service_healthy确保数据库和缓存就绪后才启动。 - 启动顺序:MySQL/Redis → Spring Boot → Nginx(Nginx 依赖应用健康)。
四、核心实现:完整的 docker-compose.yml
下面这个文件是真实项目的简化版,但关键配置全部保留。直接存为 docker-compose.yml:
services:
mysql:
image: mysql:8.0.35
container_name: order-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: root#2024
MYSQL_DATABASE: order_db
MYSQL_USER: order_app
MYSQL_PASSWORD: order_app#2024
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
volumes:
- mysql-data:/var/lib/mysql
- ./init-sql:/docker-entrypoint-initdb.d:ro # 首次启动自动执行建表SQL
networks:
- app-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot#2024"]
interval: 5s
timeout: 3s
retries: 10
start_period: 30s
redis:
image: redis:7.2.3-alpine
container_name: order-redis
restart: always
command: redis-server --appendonly yes --requirepass redis#2024
volumes:
- redis-data:/data
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis#2024", "ping"]
interval: 5s
timeout: 3s
retries: 10
app:
image: openjdk:17-jdk-alpine
container_name: order-app
restart: always
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./app.jar:/app/app.jar:ro
- ./logs:/app/logs
working_dir: /app
entrypoint: ["java", "-Xms512m", "-Xmx512m", "-jar", "app.jar", "--spring.profiles.active=prod"]
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 40s
nginx:
image: nginx:1.24.0-alpine
container_name: order-nginx
restart: always
depends_on:
app:
condition: service_healthy
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./static:/usr/share/nginx/html:ro
networks:
- app-network
volumes:
mysql-data:
name: order-mysql-data
redis-data:
name: order-redis-data
networks:
app-network:
driver: bridge
name: order-network
ipam:
config:
- subnet: 172.28.0.0/16
配套的 nginx.conf 关键片段(负载均衡 + 反向代理):
upstream order_backend {
server app:8080 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
location /api/ {
proxy_pass http://order_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
location /static/ {
alias /usr/share/nginx/html/;
expires 7d;
}
}
注意几个细节:
- depends_on 必须配合 condition: service_healthy 才能实现真正等待,否则它只控制启动顺序不控制就绪状态(这是 Compose v2 的重要改进)。
- MySQL 初始化脚本放在 ./init-sql 目录,只有数据卷为空时才会执行,重复 up 不会重复导入。
- 给 Spring Boot 分配了固定 512M 堆内存,避免容器内存溢出被 OOM Killer 杀掉。
五、踩坑与优化:四个血泪教训
坑1:healthcheck 命令必须用容器内的工具。 我最初想用 curl 检查应用健康,但 alpine 镜像里没有 curl,导致 healthcheck 永远失败。后来换成 wget(busybox 自带)才解决。同理,MySQL 的 healthcheck 用了 mysqladmin,Redis 用 redis-cli,这些都是镜像内自带的。
坑2:MySQL 初始化脚本执行超时。 我的 init-sql 里有 20 张表的建表语句,首次启动时 MySQL 容器需要 30-50 秒完成初始化,此时 healthcheck 的 start_period 必须给足,否则会被误判为 unhealthy。实测设置 start_period: 30s 比较安全。
坑3:日志文件无限增长。 应用日志直接挂载宿主机目录,跑了两周发现磁盘爆了。解决方案是在 docker-compose 里加日志轮转(虽然本文没写,但强烈建议加):
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
坑4:自定义网络子网冲突。 公司内网有个 VPN 网段恰好也是 172.28.0.0/16,导致容器无法访问外部网络。后来改成 172.28.0.0/16 不行就换 172.29.0.0/16,或者干脆不写 ipam 让 Docker 自动分配。建议除非有特殊需求,否则去掉 ipam 配置。
六、效果数据:从混乱到可控
改造后的效果非常直观:
- 服务启动时间:手动逐个启动需要 3 分 20 秒(含人工等待 MySQL 就绪),现在
docker compose up -d一键完成,全程 45 秒。其中 MySQL 初始化占 30 秒,应用启动占 15 秒。 - 数据持久化:执行
docker compose down后重新up,MySQL 数据完好,Redis 的 AOF 文件正常加载,缓存命中率 100% 无冷启动。 - 故障恢复:模拟杀掉 MySQL 容器,
restart: always自动拉起,应用连接池自动重连,业务无感知。Nginx 的max_fails=3确保单个应用实例挂掉后自动摘除,请求不中断。 - 资源占用:四个容器稳定运行 7 天,内存占用约 1.8G(MySQL 600M + Redis 200M + 应用 512M*2 + Nginx 20M),CPU 空闲时低于 5%。
七、总结与推荐实践
Docker Compose 编排多服务项目,核心就三件事:网络隔离、数据持久化、就绪检查。如果你正在维护一个超过 3 个服务的 Compose 项目,我强烈建议:
- 每个有状态服务(数据库、缓存)必须用命名卷,别偷懒用匿名卷。
- 所有依赖服务的
depends_on都写上condition: service_healthy,别只用裸的depends_on。 - 健康检查命令优先用镜像自带工具,别赌基础镜像里有 curl。
- 给所有服务加日志轮转,否则半年后你的磁盘会教你做人。
这套配置已经在我们的测试环境稳定运行 3 个月,最近刚推广到预发布环境。如果你有更好的实践,欢迎评论区交流——毕竟踩坑的路上,大家都不孤单。