一、问题背景:为什么单靠docker run会让人崩溃
项目是一个典型的后台管理系统,前端Vue打包成静态资源交给Nginx,后端Spring Boot提供API,数据层是MySQL和Redis。早期我图省事,写了个shell脚本按顺序docker run,结果每次重启机器都是一场灾难:
- MySQL容器虽然起来了,但初始化数据库要十几秒,后端连上去直接报Connection refused然后退出;
- Nginx因为找不到后端upstream,502一片;
- 容器之间只能用宿主机IP互访,换个环境IP全乱;
- 数据没做卷挂载,一次误删容器,测试数据全没了。
后来彻底转向Docker Compose。它用一份YAML描述所有服务、网络、卷和依赖关系,一条docker compose up -d就能把整套环境拉起来。下面把完整方案拆开讲。
二、环境与版本
先说清楚版本,避免你照着抄却因为版本差异踩坑:
- Docker Engine 24.0.7
- Docker Compose v2.23.0(注意是v2,命令是
docker compose而不是docker-compose) - MySQL 8.0.35
- Redis 7.2.3
- OpenJDK 17 + Spring Boot 3.2.0
- Nginx 1.25.3
这里强调一点:depends_on配合condition: service_healthy这个能力,在Compose文件格式3.x的早期版本里是被移除过的,v2重新支持并推荐使用。所以务必确认你的Compose是v2版本。
三、方案设计
整体拓扑很简单:
┌─────────┐
用户 → │ Nginx │ (80端口对外)
└────┬────┘
│ backend:8080
┌────▼────┐
│ backend │ (Spring Boot)
└──┬───┬──┘
mysql │ │ redis
┌────▼┐ ┌▼─────┐
│MySQL│ │Redis │
└─────┘ └──────┘
设计要点:
- 自定义bridge网络:所有服务加入同一个network,容器间直接用服务名当主机名解析,不依赖宿主机IP。
- 命名卷:MySQL数据、Redis持久化、Nginx日志分别挂载,容器删了数据还在。
- 健康检查:MySQL用
mysqladmin ping,Redis用redis-cli ping,backend用/actuator/health,全部配healthcheck。 - 启动顺序:backend依赖mysql和redis的
service_healthy,nginx依赖backend的service_healthy,形成严格的启动链。
四、核心实现:完整docker-compose.yml
先看目录结构:
project/
├── docker-compose.yml
├── .env
├── backend/
│ └── Dockerfile
├── nginx/
│ ├── Dockerfile
│ └── nginx.conf
└── mysql/
└── init.sql
.env文件放敏感配置,不提交到git:
MYSQL_ROOT_PASSWORD=Root@123456
MYSQL_DATABASE=admin_db
MYSQL_USER=app_user
MYSQL_PASSWORD=App@123456
REDIS_PASSWORD=Redis@123456
下面是完整的docker-compose.yml:
version: "3.9"
services:
mysql:
image: mysql:8.0.35
container_name: admin-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
- --max_connections=500
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- admin-net
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
redis:
image: redis:7.2.3
container_name: admin-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
volumes:
- redis-data:/data
networks:
- admin-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
backend:
build:
context: ./backend
dockerfile: Dockerfile
image: admin-backend:1.0.0
container_name: admin-backend
restart: unless-stopped
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: ${MYSQL_USER}
SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD}
JAVA_OPTS: "-Xms512m -Xmx1024m"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- admin-net
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/actuator/health | grep -q '\"status\":\"UP\"'"]
interval: 15s
timeout: 5s
retries: 5
start_period: 60s
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
image: admin-nginx:1.0.0
container_name: admin-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- nginx-logs:/var/log/nginx
depends_on:
backend:
condition: service_healthy
networks:
- admin-net
volumes:
mysql-data:
driver: local
redis-data:
driver: local
nginx-logs:
driver: local
networks:
admin-net:
driver: bridge
后端Dockerfile用多阶段构建,减小镜像体积:
FROM maven:3.9.5-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
注意backend的healthcheck里用到了wget,所以我在运行阶段显式装了它。这是很多人会忽略的坑:基础镜像里没wget,健康检查永远失败,backend永远不healthy,nginx就永远起不来。
五、踩坑与优化
坑1:depends_on不等待健康状态
最初我只写了:
depends_on:
- mysql
- redis
结果Compose只保证容器"启动",不保证"就绪"。backend启动时MySQL还在初始化,直接连接失败退出。加上condition: service_healthy后才真正解决问题。
坑2:MySQL健康检查的密码变量转义
healthcheck里如果写-p$MYSQL_ROOT_PASSWORD,$会被Compose当成变量插值处理,导致命令变成空密码。必须写成$$MYSQL_ROOT_PASSWORD,用双美元符转义,让它在容器内被shell解析。
坑3:start_period设置太短
MySQL 8.0首次初始化数据目录(执行init.sql)在我的机器上要25秒左右。一开始start_period只给了10秒,健康检查还没等到就判定失败。后来调到30秒,配合retries: 10才稳定。这个值要按你init.sql的复杂度调整。
坑4:Nginx的upstream要用服务名
nginx.conf里后端地址必须写backend:8080,而不是localhost:8080或某个IP。因为Nginx容器和backend容器是平级的,靠Docker内置DNS解析服务名。
upstream backend_server {
server backend:8080;
keepalive 32;
}
server {
listen 80;
location /api/ {
proxy_pass http://backend_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
优化点:
- JVM参数显式限制堆内存
-Xms512m -Xmx1024m,避免容器里JVM按宿主机内存分配导致OOM。 - MySQL连接池HikariCP的
maximum-pool-size设为20,配合max_connections=500绰绰有余。 - 所有服务加
restart: unless-stopped,机器重启后自动拉起。
六、效果数据
在4核8G的测试机上实测:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 冷启动到全部healthy | 经常失败 | 约42秒 |
| 数据库连接失败次数 | 每次重启必现 | 0 |
| 一键部署命令 | 多步脚本 | docker compose up -d |
| 镜像总体积 | 约1.8GB | 约1.1GB(多阶段构建) |
查看服务状态:
$ docker compose ps
NAME STATUS PORTS
admin-mysql Up (healthy) 3306/tcp
admin-redis Up (healthy) 6379/tcp
admin-backend Up (healthy) 8080/tcp
admin-nginx Up (healthy) 0.0.0.0:80->80/tcp
四个服务全是healthy,心里踏实多了。日志排查用docker compose logs -f backend,重建单个服务用docker compose up -d --build backend,不影响其他容器。
七、总结
Docker Compose编排的核心,说到底就是把"人肉维护的启动顺序"变成"声明式的依赖关系"。几个关键经验:
- 只要服务之间有依赖,就用
condition: service_healthy,别偷懒只写服务名。 - healthcheck的
start_period一定要给足,尤其是MySQL这种初始化慢的。 - 健康检查命令依赖的工具(wget/curl)必须确保镜像里有。
- 数据一律走命名卷,别用匿名卷,否则清理时容易误删。
- Compose用v2,命令是
docker compose,别再用老的docker-compose。
这套配置我现在用在三个项目里,换台机器只要装好Docker,git clone加docker compose up -d就能跑起来,省下的时间够多写不少业务代码了。