一、问题背景:5条docker run命令部署的噩梦
上个月接手一个电商项目,后端是Spring Boot,依赖MySQL、Redis,前面挂着Nginx做反向代理。部署环境是阿里云ECS,2核4G配置。之前运维同事离职,留下一个deploy.sh脚本,里面是5条docker run命令。每次部署都要按顺序执行,还要手动挂载卷、创建网络、等MySQL就绪才能启动Spring Boot。最痛苦的是:MySQL和Redis的数据卷目录是散的,分布在/var/lib/docker/volumes下,根本分不清哪个卷属于哪个服务;网络用的是默认bridge,容器之间靠IP访问,IP一变就得改配置。
有一次半夜线上告警,排查发现是Redis容器重启后IP变了,Spring Boot配置的Redis地址失效。从那天起,我决定必须用Docker Compose重写部署。
二、环境与版本
- Docker Engine: 24.0.7
- Docker Compose: v2.23.3
- Spring Boot: 3.2.1 (Java 17)
- MySQL: 8.2.0
- Redis: 7.2.3
- Nginx: 1.25.3
项目结构:
ecommerce/
├── backend/ # Spring Boot应用
│ ├── Dockerfile
│ └── target/app.jar
├── frontend/ # 前端静态文件
│ └── dist/
├── nginx/
│ └── conf.d/
│ └── ecommerce.conf
└── docker-compose.yml
三、方案设计:网络、卷、健康检查三要素
核心设计思路分三块:
1. 自定义bridge网络
用docker network create的等价声明式配置,让服务之间通过服务名互相访问,彻底告别IP硬编码。
2. 命名卷持久化
MySQL和Redis的数据必须落盘,用volumes:顶级声明命名卷,容器删了数据还在。
3. healthcheck + depends_on条件启动
Spring Boot容器启动前必须确认MySQL和Redis已就绪,不能用depends_on的简单形式(它只检查容器启动,不检查服务可用)。用condition: service_healthy让Compose等待健康检查通过。
四、核心实现:docker-compose.yml完整配置
先看完整文件,docker-compose.yml放在项目根目录:
version: '3.8'
services:
mysql:
image: mysql:8.2.0
container_name: ecommerce-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123}
MYSQL_DATABASE: ecommerce
MYSQL_USER: ecommerce_user
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-user123}
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/init:/docker-entrypoint-initdb.d:ro
networks:
- ecommerce-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-root123}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
ports:
- "3306:3306"
redis:
image: redis:7.2.3-alpine
container_name: ecommerce-redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123}
volumes:
- redis_data:/data
networks:
- ecommerce-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis123}", "ping"]
interval: 5s
timeout: 3s
retries: 5
backend:
build:
context: ./backend
dockerfile: Dockerfile
image: ecommerce-backend:1.0.0
container_name: ecommerce-backend
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ecommerce?useSSL=false&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: ecommerce_user
SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD:-user123}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD:-redis123}
networks:
- ecommerce-net
ports:
- "8080:8080"
nginx:
image: nginx:1.25.3-alpine
container_name: ecommerce-nginx
restart: unless-stopped
depends_on:
- backend
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./frontend/dist:/usr/share/nginx/html:ro
networks:
- ecommerce-net
ports:
- "80:80"
- "443:443"
networks:
ecommerce-net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
volumes:
mysql_data:
driver: local
redis_data:
driver: local
注意几个关键点:
depends_on条件启动:condition: service_healthy会阻塞backend启动,直到mysql和redis的healthcheck连续通过。- MySQL健康检查:用
mysqladmin ping,注意要传密码,否则会报Access denied。 - Redis健康检查:
redis-cli ping需要-a传密码,否则返回NOAUTH错误。 - 网络子网固定:手动指定
subnet: 172.28.0.0/16,避免和ECS上其他docker网络冲突。 - 初始化脚本:
./mysql/init目录放建表SQL,首次启动时自动执行。
后端Dockerfile(简化版):
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
五、踩坑与优化:三个血泪教训
坑1:MySQL健康检查一直失败
一开始healthcheck写的是mysqladmin ping但不带密码,结果容器一直处于unhealthy状态,backend卡在等待。查日志才发现是权限问题,加上-u root -p密码就好了。另外start_period: 30s很关键,MySQL首次初始化要20多秒,这段时间内失败不计入重试次数。
坑2:Redis密码导致健康检查误判
Redis启动命令里设了--requirepass,但healthcheck没带密码,导致PING返回NOAUTH。加了-a参数后恢复正常。这里有个安全细节:密码用环境变量引用,不要硬编码在compose文件里。
坑3:卷挂载权限问题
Nginx的配置文件挂载时用了:ro只读,但前端静态文件目录如果宿主机的dist目录权限不对,Nginx会报403。解决方法是构建时先chmod -R 755前端文件,或者挂载时加:ro保证安全。
优化点:启动速度
手动部署时,等MySQL就绪要轮询检测,最快也要2-3分钟。用healthcheck + depends_on后,docker compose up -d完成后台服务全部拉起,实测平均40秒内所有容器达到healthy状态。如果MySQL是全新初始化,首次启动会多花20秒左右。
六、效果数据与对比
部署这套配置后,我记录了三个关键指标:
| 指标 | 手动docker run | Docker Compose |
|---|---|---|
| 部署命令数 | 5条 + 等待脚本 | 1条 |
| 服务就绪时间 | 平均3分15秒 | 平均42秒 |
| 数据持久化可靠性 | 目录混乱,易丢失 | 命名卷管理,docker volume ls清晰可见 |
| 配置变更 | 需要改多处脚本 | 改compose文件一处 |
另外,docker compose ps一眼看出每个服务的健康状态,docker compose logs -f backend实时看日志,排查问题效率提升了一个档次。
七、总结
Docker Compose不是简单把docker run命令搬进YAML,而是用声明式语法把服务依赖、网络、存储、健康检查这些运维逻辑固化下来。这套配置跑在生产环境两个月,经历了两次ECS重启和一次MySQL容器意外删除,数据卷完好,服务自动恢复。如果项目超过3个服务,强烈建议用Compose编排,省下的运维时间远超学习成本。
最后提醒一句:永远不要在生产环境用docker run裸奔,哪怕只有一个服务,也要用Compose管理。这是用加班换来的教训。