1. 问题背景:单机脚本部署的失控现场
上个月接手一个电商后台项目,4个服务(Spring Boot应用、Redis缓存、MySQL数据库、Nginx静态资源)原本靠一堆 .sh 脚本启动。每次发布新版本,运维同事都要手动敲 docker run,顺序稍错就报 Connection refused。更头疼的是MySQL还没初始化完,应用容器已经开始连库,导致接口超时率飙升到18%。
我决定用Docker Compose统一编排,但很快发现:Compose默认的 depends_on 只保证容器创建顺序,不保证服务就绪。比如Redis容器起来了,但PING还没响应,应用去连接照样失败。需要一套完整的健康检查机制。
2. 环境与版本:先亮底牌
- 宿主机:Ubuntu 22.04.3 LTS(内核5.15.0)
- Docker Engine:24.0.7(已启用buildkit)
- Docker Compose:v2.24.2(
docker compose子命令,非旧版docker-compose) - 镜像版本:
mysql:8.0.35(使用mysql:8.0而非latest,避免升级破坏)redis:7.2.3-alpine(alpine版体积小37%)openjdk:17-jdk-slim(应用基础镜像)nginx:1.25.3-alpine
3. 方案设计:四层防护确保启动可靠性
核心设计思路分四块:
- 自定义网络:创建
app_net桥接网络,容器间通过服务名互访,不暴露数据库端口到宿主机。 - 卷挂载:MySQL数据目录、Redis持久化文件、Nginx静态资源目录全部挂载到宿主机,容器销毁不丢数据。
- 健康检查:每个依赖服务定义
healthcheck,应用容器等所有依赖healthy后才启动。 - 启动顺序:用
depends_on的condition: service_healthy替代默认的service_started。
网络拓扑如下(ASCII简图):
[nginx:80] → [app:8080] → [mysql:3306]
↘ [redis:6379]
(所有容器在 app_net 网络内,宿主机仅暴露 80 端口)
4. 核心实现:可运行的docker-compose.yml
直接上配置文件。注意 mysql 的初始化脚本目录 ./init-sql 挂载到 /docker-entrypoint-initdb.d/,首次启动自动执行建表语句。
version: '3.8'
networks:
app_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16 # 自定义子网,避免与宿主机网段冲突
volumes:
mysql_data: # 命名卷,持久化MySQL数据
redis_data: # 命名卷,持久化Redis RDB文件
services:
mysql:
image: mysql:8.0.35
container_name: ecom-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123}
MYSQL_DATABASE: ecom
MYSQL_USER: ecom_app
MYSQL_PASSWORD: ecom_pass
volumes:
- mysql_data:/var/lib/mysql
- ./init-sql:/docker-entrypoint-initdb.d:ro # 首次启动自动执行SQL
networks:
app_net:
ipv4_address: 172.28.0.10 # 固定IP,方便排查
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s # 每5秒检查一次
timeout: 3s # 单次检查超时3秒
retries: 12 # 连续失败12次判定为unhealthy(约60秒)
start_period: 30s # 容器启动后30秒内不检查,给MySQL初始化时间
redis:
image: redis:7.2.3-alpine
container_name: ecom-redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis_pass}
volumes:
- redis_data:/data
networks:
app_net:
ipv4_address: 172.28.0.11
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_pass}", "ping"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
app:
build: ./app
image: ecom-app:1.0.0
container_name: ecom-app
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy # 关键:等MySQL健康后才启动
redis:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ecom?useSSL=false
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_pass}
networks:
app_net:
ipv4_address: 172.28.0.12
ports:
- "127.0.0.1:8080:8080" # 只绑定本机,由Nginx反向代理
nginx:
image: nginx:1.25.3-alpine
container_name: ecom-nginx
restart: unless-stopped
depends_on:
app:
condition: service_started # 应用容器启动即可,不需要等健康
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./static:/usr/share/nginx/html:ro
ports:
- "80:80"
networks:
app_net:
ipv4_address: 172.28.0.13
启动命令(在项目根目录执行):
# 创建环境变量文件 .env(示例)
cat > .env << EOF
MYSQL_ROOT_PASSWORD=my_root_pw
REDIS_PASSWORD=my_redis_pw
EOF
# 后台启动所有服务
docker compose up -d --build
# 查看启动状态(注意STATUS列显示healthy)
docker compose ps
# 实时查看日志
docker compose logs -f app
5. 踩坑与优化:三个真实教训
坑1:healthcheck的start_period没设置,导致MySQL被误杀
第一次配置时只写了 interval 和 retries,结果MySQL容器启动后30秒内还没初始化完,healthcheck连续失败,Compose直接判定unhealthy。应用容器启动后连库失败,整个链路崩了。
优化:给MySQL设置 start_period: 30s,给Redis设置 start_period: 10s(Redis启动快)。现在测试,MySQL首次启动约25秒后healthy,应用容器准时拉起。
坑2:depends_on只写服务名,不写condition
Compose默认的 depends_on 只确保依赖容器先启动,但MySQL容器启动后可能需要20秒才能接受连接。应用容器先启动就会报 Communications link failure。
优化:必须显式写 condition: service_healthy。注意旧版 docker-compose(v1)不支持这个语法,必须用 docker compose(v2)。
坑3:Nginx静态资源卷挂载权限问题
把宿主机的 ./static 目录挂载到容器内Nginx的html目录,结果容器内Nginx用户(uid=101)无法读取宿主机文件(uid=1000)。页面403。
优化:两种解法——要么在宿主机执行 chown -R 101:101 ./static,要么在compose中加 user: root(不推荐)。我选了前者,更安全。
6. 效果数据:数字说话
改造前后对比(同一台机器,压测工具JMeter,500并发持续10分钟):
| 指标 | 改造前(shell脚本) | 改造后(Compose) | 提升 |
|---|---|---|---|
| 服务启动成功率 | 82% | 99.5% | +17.5% |
| 平均启动耗时 | 3分20秒 | 2分05秒 | -37% |
| 接口首次可用时间 | 4分钟后 | 2分30秒 | -37.5% |
| 容器重启恢复时间 | 手动干预 | 自动重启+健康检查 | 无人工 |
另外,因为MySQL和Redis数据都在命名卷里,docker compose down 不会删数据,升级镜像版本后数据还在。发布流程从原先的10分钟缩短到3分钟。
7. 总结与思考
用Compose编排多服务,核心不是写YAML,而是理解容器生命周期。depends_on 不是银弹,真正可靠的是healthcheck + condition的组合拳。我踩过的坑基本都源于对服务就绪时间的误判。
几个建议:
- 生产环境别用 latest 标签,锁死版本号
- 数据库密码用环境变量注入,别写死在YAML里
- 健康检查的命令要轻量,别用复杂脚本
- 网络里固定IP虽然方便排查,但多环境部署时容易冲突,建议只在测试环境用
下一步我准备把这份配置迁移到K8s,用Deployment + Service替代Compose。到时候再来分享K8s版的编排实践。
(附:完整项目代码已上传GitHub,仓库地址:github.com/yourname/ecom-docker-compose,包含init-sql初始化脚本和Nginx配置示例。)