一、问题背景
去年团队接手一个前后端分离项目,技术栈是Vue 3 + Spring Boot 3.2 + MySQL 8.0 + Redis 7。开发环境大家各跑各的,勉强能凑合。但一到测试环境部署就头疼:
- 手动装MySQL、Redis,配置参数靠口口相传,新同事经常配错端口
- 后端启动时MySQL还没初始化完,直接报连接超时,得重启好几次
- 每次部署要25分钟以上,还容易漏步骤
- 服务器上残留的容器、网络、卷越来越多,清理全靠
docker rm -f $(docker ps -aq)这种暴力操作
痛定思痛,决定用Docker Compose把整套编排固化下来。目标很明确:一条docker compose up -d搞定所有服务,启动顺序自动控制,数据持久化不丢,健康状态可观测。
二、环境与版本
先交代下环境,避免版本差异导致踩坑:
| 组件 | 版本 | 说明 |
|---|---|---|
| 操作系统 | Ubuntu 22.04 LTS | 内核5.15 |
| Docker Engine | 24.0.7 | 支持Compose V2 |
| Docker Compose | v2.23.0 | 用docker compose而非docker-compose |
| MySQL | 8.0.35 | 官方镜像 |
| Redis | 7.2.3-alpine | 轻量版 |
| OpenJDK | 17 (eclipse-temurin:17-jre) | Spring Boot 3.2要求 |
| Nginx | 1.25.3-alpine | 静态资源+反向代理 |
注意:Compose V2和V1在语法上有差异,比如
version字段在V2里已经废弃,depends_on的condition写法也有变化。本文全部基于V2。
三、方案设计
整体架构分四层:
[Nginx] → [Spring Boot App] → [MySQL]
↓
[Redis]
设计要点:
- 网络隔离:自定义bridge网络
app-net,四个服务都接入,通过服务名互相访问(Docker内置DNS)。不暴露MySQL和Redis端口到宿主机,只在内网通信。 - 数据持久化:MySQL数据、Redis数据、Nginx日志分别用命名卷挂载,容器删了数据还在。
- 健康检查:MySQL用
mysqladmin ping,Redis用redis-cli ping,App用/actuator/health端点,Nginx用wget探测。 - 启动顺序:用
depends_on的condition: service_healthy,确保MySQL和Redis完全就绪后App才启动,App健康后Nginx才启动。 - 配置外置:敏感信息(数据库密码等)走
.env文件,不硬编码在compose里。
四、核心实现
先看目录结构:
project/
├── docker-compose.yml
├── .env
├── nginx/
│ ├── nginx.conf
│ └── conf.d/default.conf
├── app/
│ └── Dockerfile
└── mysql/
└── init.sql
4.1 完整的 docker-compose.yml
services:
mysql:
image: mysql:8.0.35
container_name: app-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
- --innodb_buffer_pool_size=512M
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- app-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: 40s
redis:
image: redis:7.2.3-alpine
container_name: app-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
volumes:
- redis-data:/data
networks:
- app-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
app:
build:
context: ./app
dockerfile: Dockerfile
image: myapp-backend:1.0.0
container_name: app-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 -XX:+UseG1GC"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app-net
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/actuator/health"]
interval: 15s
timeout: 5s
retries: 5
start_period: 60s
nginx:
image: nginx:1.25.3-alpine
container_name: app-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./dist:/usr/share/nginx/html:ro
- nginx-logs:/var/log/nginx
depends_on:
app:
condition: service_healthy
networks:
- app-net
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost/healthz"]
interval: 15s
timeout: 3s
retries: 3
start_period: 10s
networks:
app-net:
driver: bridge
name: app-net
volumes:
mysql-data:
name: app-mysql-data
redis-data:
name: app-redis-data
nginx-logs:
name: app-nginx-logs
4.2 .env 文件
MYSQL_ROOT_PASSWORD=Root@2024!Secure
MYSQL_DATABASE=myapp
MYSQL_USER=appuser
MYSQL_PASSWORD=App@2024!Pass
REDIS_PASSWORD=Redis@2024!Pass
注意.env要加进.gitignore,生产环境用CI/CD的secret注入。
4.3 后端 Dockerfile
FROM eclipse-temurin:17-jre-jammy
RUN apt-get update && apt-get install -y --no-install-recommends wget \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -r -u 1001 appuser
WORKDIR /app
COPY target/myapp-1.0.0.jar app.jar
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
这里装wget是为了健康检查用(eclipse-temurin基础镜像不带)。用非root用户跑是安全最佳实践。
五、踩坑与优化
坑1:MySQL健康检查一直unhealthy
一开始写的是:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
问题在于localhost在容器里可能走socket连接,MySQL初始化阶段socket还没就绪。改成127.0.0.1强制走TCP,并加上start_period: 40s(MySQL首次初始化要建表、建用户,40秒是保守值,实测在2C4G机器上约25秒)。
坑2:depends_on不等待健康
Compose V2里如果只写:
depends_on:
- mysql
那只是保证启动顺序,不保证MySQL已就绪。必须用map形式加condition: service_healthy。这是新手最容易踩的坑。
坑3:Redis密码在健康检查里不生效
redis-cli ping如果Redis设了密码会返回NOAUTH Authentication required。必须加-a ${REDIS_PASSWORD}。但这样密码会出现在docker inspect里,介意的话可以用REDISCLI_AUTH环境变量替代。
坑4:Nginx启动后502
原因是App虽然healthy了,但Spring Boot的/actuator/health返回UP时,Tomcat可能还没完全接受连接。解决方案是在Nginx配置里加proxy_next_upstream重试,或者给App的start_period留足余量(我们给了60秒)。实际项目里,Spring Boot冷启动约18秒,60秒足够。
优化:日志卷挂载
Nginx日志用命名卷nginx-logs,避免容器删了日志也没了。查看日志:
docker run --rm -v app-nginx-logs:/logs alpine cat /logs/access.log
或者直接docker logs app-nginx。
优化:资源限制
生产环境建议加deploy.resources:
deploy:
resources:
limits:
cpus: '2.0'
memory: 1.5G
reservations:
memory: 512M
注意在非Swarm模式下,deploy.resources.limits在Compose V2.23+才完全支持,早期版本要用mem_limit和cpus。
六、效果数据
部署前后对比(同一台4C8G阿里云ECS,Ubuntu 22.04):
| 指标 | 手动部署 | Compose编排 |
|---|---|---|
| 首次部署耗时 | 25分钟 | 6分12秒(含镜像拉取) |
| 增量部署耗时 | 8分钟 | 47秒 |
| 启动失败率 | 约15% | <1% |
| 服务启动顺序错误 | 经常发生 | 0 |
| 数据丢失风险 | 中 | 无(命名卷) |
| 新同事上手时间 | 半天 | 10分钟 |
健康检查参数实测数据:
- MySQL start_period: 40s,实测首次启动28秒,留了12秒余量
- App start_period: 60s,实测18秒,余量充足
- 全链路从docker compose up -d到Nginx可访问:约75秒
七、总结
Docker Compose编排多服务,核心就三件事:
- 网络:自定义bridge,服务名即主机名,不暴露内部端口到宿主机
- 卷:命名卷持久化数据,绑定挂载放配置和静态资源
- 启动顺序:
depends_on+condition: service_healthy+ 合理的start_period
健康检查的start_period宁可给大点,不要给小。大点只是多等几秒,小点会导致服务反复重启,反而更慢。
最后提醒一句:Compose适合单机编排,如果要多机集群,还是得上K8s。但对中小项目来说,Compose的性价比极高——配置文件不到150行,解决了80%的部署痛点。
完整配置已上传到GitHub(示例项目地址),有需要的同学自取。有问题欢迎评论区交流。