1. 引言
这是系列实战的收官篇。前面几篇我们分别搞定了基础环境、后端服务、前端构建,现在到了把它们串起来的时候。
本篇文章的目标很简单:用一套 Docker Compose 文件,把 MySQL、Redis、后端、前端、Nginx 全部跑起来,做到 clone 即跑。
你将会得到:
- 一个完整可复制的 compose 栈:数据库 + 缓存 + 后端 + 前端 + 反向代理;
- 初始化 SQL 注入、健康检查、热更新挂载、端口规划的全套实践;
- 配套 README:启动 / 停止 / 重置 / 备份四个操作;
- 数据与配置的"开发-生产"双模式切换。
前置依赖:本系列第 03 篇(Docker 基础)、第 05 篇(后端容器化)、第 07 篇(前端容器化)。如果你还没看过,建议先补上。
2. 整体架构
先看我们要搭什么。一个典型的全栈开发环境包含五个角色:
| 角色 | 技术选型 | 作用 |
|---|---|---|
| 数据库 | MySQL 8 | 持久化业务数据 |
| 缓存 | Redis 7 | 会话、热点数据、队列 |
| 后端 | Spring Boot / Node(按你的技术栈) | 业务逻辑 |
| 前端 | Vite / Webpack 构建产物 | 静态页面 |
| 反向代理 | Nginx | 统一入口、路由、静态资源 |
它们之间的关系:
#mermaid-svg-QX23DUxgdtJOVg8C{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QX23DUxgdtJOVg8C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QX23DUxgdtJOVg8C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QX23DUxgdtJOVg8C .error-icon{fill:#552222;}#mermaid-svg-QX23DUxgdtJOVg8C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QX23DUxgdtJOVg8C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QX23DUxgdtJOVg8C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QX23DUxgdtJOVg8C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QX23DUxgdtJOVg8C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QX23DUxgdtJOVg8C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QX23DUxgdtJOVg8C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QX23DUxgdtJOVg8C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QX23DUxgdtJOVg8C .marker.cross{stroke:#333333;}#mermaid-svg-QX23DUxgdtJOVg8C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QX23DUxgdtJOVg8C p{margin:0;}#mermaid-svg-QX23DUxgdtJOVg8C .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QX23DUxgdtJOVg8C .cluster-label text{fill:#333;}#mermaid-svg-QX23DUxgdtJOVg8C .cluster-label span{color:#333;}#mermaid-svg-QX23DUxgdtJOVg8C .cluster-label span p{background-color:transparent;}#mermaid-svg-QX23DUxgdtJOVg8C .label text,#mermaid-svg-QX23DUxgdtJOVg8C span{fill:#333;color:#333;}#mermaid-svg-QX23DUxgdtJOVg8C .node rect,#mermaid-svg-QX23DUxgdtJOVg8C .node circle,#mermaid-svg-QX23DUxgdtJOVg8C .node ellipse,#mermaid-svg-QX23DUxgdtJOVg8C .node polygon,#mermaid-svg-QX23DUxgdtJOVg8C .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QX23DUxgdtJOVg8C .rough-node .label text,#mermaid-svg-QX23DUxgdtJOVg8C .node .label text,#mermaid-svg-QX23DUxgdtJOVg8C .image-shape .label,#mermaid-svg-QX23DUxgdtJOVg8C .icon-shape .label{text-anchor:middle;}#mermaid-svg-QX23DUxgdtJOVg8C .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QX23DUxgdtJOVg8C .rough-node .label,#mermaid-svg-QX23DUxgdtJOVg8C .node .label,#mermaid-svg-QX23DUxgdtJOVg8C .image-shape .label,#mermaid-svg-QX23DUxgdtJOVg8C .icon-shape .label{text-align:center;}#mermaid-svg-QX23DUxgdtJOVg8C .node.clickable{cursor:pointer;}#mermaid-svg-QX23DUxgdtJOVg8C .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QX23DUxgdtJOVg8C .arrowheadPath{fill:#333333;}#mermaid-svg-QX23DUxgdtJOVg8C .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QX23DUxgdtJOVg8C .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QX23DUxgdtJOVg8C .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QX23DUxgdtJOVg8C .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QX23DUxgdtJOVg8C .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QX23DUxgdtJOVg8C .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QX23DUxgdtJOVg8C .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QX23DUxgdtJOVg8C .cluster text{fill:#333;}#mermaid-svg-QX23DUxgdtJOVg8C .cluster span{color:#333;}#mermaid-svg-QX23DUxgdtJOVg8C div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QX23DUxgdtJOVg8C .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QX23DUxgdtJOVg8C rect.text{fill:none;stroke-width:0;}#mermaid-svg-QX23DUxgdtJOVg8C .icon-shape,#mermaid-svg-QX23DUxgdtJOVg8C .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QX23DUxgdtJOVg8C .icon-shape p,#mermaid-svg-QX23DUxgdtJOVg8C .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QX23DUxgdtJOVg8C .icon-shape .label rect,#mermaid-svg-QX23DUxgdtJOVg8C .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QX23DUxgdtJOVg8C .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QX23DUxgdtJOVg8C .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QX23DUxgdtJOVg8C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 浏览器
Nginx 反向代理 :80
前端静态资源
后端 API :8080
MySQL :3306
Redis :6379
Nginx 是唯一对外暴露的端口,浏览器只跟它打交道。前端静态资源由 Nginx 直接托管,/api 开头的请求反向代理到后端容器,后端再访问 MySQL 和 Redis。
3. 端口规划
端口规划是 compose 栈最容易踩坑的地方。原则很简单:宿主机端口尽量不暴露,容器间用内部网络通信。
3.1 开发模式端口
| 服务 | 容器内端口 | 宿主机映射 | 说明 |
|---|---|---|---|
| Nginx | 80 | 8080 | 唯一对外入口 |
| 后端 | 8080 | 不映射 | 仅容器内访问 |
| MySQL | 3306 | 3306 | 开发时方便用 Navicat 连接 |
| Redis | 6379 | 6379 | 开发时方便用客户端查看 |
3.2 生产模式端口
生产模式只暴露 Nginx 的 443/80,MySQL 和 Redis 完全不映射到宿主机,避免数据库直接暴露公网。
3.3 端口冲突排查
如果你本机已经装了 MySQL 或 Redis,3306/6379 很可能被占用。两个解决办法:
- 开发模式改映射端口,比如
3307:3306; - 用
docker compose ps查看占用情况,先停掉冲突容器。
坑位 1:端口规划冲突。建议在 README 里明确写清楚每个端口的作用,避免团队协作时互相踩。
4. 目录结构
先规划好工程目录,这是"clone 即跑"的基础:
fullstack-demo/
├── docker-compose.yml # 主编排文件
├── docker-compose.prod.yml # 生产模式覆盖文件
├── .env # 环境变量(开发)
├── .env.prod # 环境变量(生产)
├── README.md # 操作手册
├── mysql/
│ ├── init/
│ │ └── 01-init.sql # 初始化 SQL(首次启动自动执行)
│ └── conf/
│ └── my.cnf # MySQL 自定义配置
├── backend/
│ ├── Dockerfile
│ └── src/ # 后端源码
├── frontend/
│ ├── Dockerfile
│ └── dist/ # 构建产物(或源码+构建)
└── nginx/
├── nginx.conf # 反向代理配置
└── conf.d/
└── default.conf
5. docker-compose.yml 主文件
下面给出完整可复制的 compose 文件。这是核心,逐段解释。
5.1 基础声明
yaml
version: "3.8"
name: fullstack-demo
services:
# ... 各服务定义见下文
5.2 MySQL 服务
yaml
mysql:
image: mysql:8.0
container_name: demo-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- "${MYSQL_PORT}:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/init:/docker-entrypoint-initdb.d
- ./mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
几个要点:
- 初始化 SQL 注入 :
/docker-entrypoint-initdb.d目录下的.sql文件会在首次启动时按文件名顺序自动执行。注意是首次,数据卷已存在就不会再执行。 - 健康检查 :
mysqladmin ping是官方推荐方式,start_period给足 MySQL 冷启动时间。 - 数据持久化 :
mysql_data命名卷保存数据,容器删了数据还在。
5.3 Redis 服务
yaml
redis:
image: redis:7-alpine
container_name: demo-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
ports:
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
--appendonly yes 开启 AOF 持久化,开发环境足够用。redis-cli ping 返回 PONG 即健康。
5.4 后端服务
yaml
backend:
build: ./backend
container_name: demo-backend
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: ${MYSQL_USER}
SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
volumes:
- ./backend/src:/app/src
# 开发模式热更新挂载,生产模式由 prod 文件覆盖
坑位 2 :容器内 localhost 误区。后端连接 MySQL 时,绝对不能写
localhost或127.0.0.1,要写服务名mysql。因为容器内的 localhost 指向容器自己,不是宿主机。Compose 会自动做 DNS 解析,服务名即主机名。
5.5 前端服务
yaml
frontend:
build: ./frontend
container_name: demo-frontend
restart: unless-stopped
volumes:
- ./frontend/dist:/usr/share/nginx/html:ro
depends_on:
- backend
开发模式下,前端构建产物通过挂载直接进 Nginx 的静态目录。改完前端代码重新构建,刷新即生效。
5.6 Nginx 反向代理
yaml
nginx:
image: nginx:1.25-alpine
container_name: demo-nginx
restart: unless-stopped
ports:
- "${NGINX_PORT}:80"
volumes:
- ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro
- frontend_dist:/usr/share/nginx/html:ro
depends_on:
- frontend
- backend
5.7 网络与卷声明
yaml
networks:
default:
name: fullstack-demo-net
volumes:
mysql_data:
redis_data:
frontend_dist:
所有服务默认加入同一个网络,容器间通过服务名互通,无需额外配置。
6. Nginx 反向代理配置
nginx/conf.d/default.conf:
nginx
server {
listen 80;
server_name localhost;
# 前端静态资源
root /usr/share/nginx/html;
index index.html;
# 前端路由(SPA history 模式)
location / {
try_files $uri $uri/ /index.html;
}
# API 反向代理
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
}
注意 proxy_pass http://backend:8080,这里 backend 是 compose 服务名,Nginx 容器通过内部网络解析到后端容器。
7. 环境变量与双模式切换
7.1 .env(开发模式)
bash
# 数据库
MYSQL_ROOT_PASSWORD=root123456
MYSQL_DATABASE=demo
MYSQL_USER=demo
MYSQL_PASSWORD=demo123456
MYSQL_PORT=3306
# Redis
REDIS_PORT=6379
# Nginx
NGINX_PORT=8080
7.2 .env.prod(生产模式)
bash
# 数据库(生产用强密码)
MYSQL_ROOT_PASSWORD=ChangeMe_Strong_Password
MYSQL_DATABASE=demo
MYSQL_USER=demo
MYSQL_PASSWORD=ChangeMe_Strong_Password
# 生产不映射数据库端口
MYSQL_PORT=127.0.0.1:3306
REDIS_PORT=127.0.0.1:6379
# Nginx 对外端口
NGINX_PORT=80
7.3 docker-compose.prod.yml
yaml
services:
mysql:
ports:
- "${MYSQL_PORT}:3306"
redis:
ports:
- "${REDIS_PORT}:6379"
backend:
build:
context: ./backend
dockerfile: Dockerfile.prod
volumes: []
environment:
SPRING_PROFILES_ACTIVE: prod
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
volumes: []
nginx:
ports:
- "${NGINX_PORT}:80"
- "443:443"
volumes:
- ./nginx/conf.d/prod.conf:/etc/nginx/conf.d/default.conf:ro
7.4 切换命令
bash
# 开发模式
docker compose --env-file .env up -d
# 生产模式
docker compose --env-file .env.prod -f docker-compose.yml -f docker-compose.prod.yml up -d --build
坑位 3 :MySQL 8 认证插件兼容。MySQL 8 默认用
caching_sha2_password,老客户端(如某些版本的 Navicat、旧版 JDBC 驱动)会报Authentication plugin 'caching_sha2_password' cannot be loaded。解决办法:连接串加allowPublicKeyRetrieval=true,或建用户时指定mysql_native_password。
8. 初始化 SQL 注入
mysql/init/01-init.sql:
sql
-- 创建业务表
CREATE TABLE IF NOT EXISTS `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`email` VARCHAR(100),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入初始数据
INSERT INTO `user` (`username`, `email`) VALUES
('admin', 'admin@example.com'),
('demo', 'demo@example.com');
-- 创建索引
CREATE INDEX idx_user_email ON `user`(`email`);
注意:只有数据卷为空时才会执行 。想重新执行,需要先 docker compose down -v 清掉数据卷。
9. 热更新挂载
开发模式下,源码目录直接挂载进容器,改代码即时生效:
yaml
# 后端热更新(以 Spring Boot DevTools 为例)
volumes:
- ./backend/src:/app/src
# 前端热更新(Vite 开发服务器)
volumes:
- ./frontend/src:/app/src
后端配合 DevTools 自动重启,前端配合 Vite HMR 秒级刷新。生产模式用 volumes: [] 覆盖掉挂载,改用镜像内构建产物。
10. README:四个核心操作
配套 README 是"clone 即跑"的最后一块拼图。四个操作必须写清楚:
10.1 启动
bash
git clone <repo-url>
cd fullstack-demo
cp .env.example .env
docker compose up -d
首次启动会自动构建镜像、初始化数据库。等所有容器 healthy 后访问 http://localhost:8080。
10.2 停止
bash
# 停止但不删除容器
docker compose stop
# 停止并删除容器(保留数据卷)
docker compose down
10.3 重置
bash
# 彻底重置:删除容器 + 数据卷,下次启动重新初始化
docker compose down -v
docker compose up -d
警告:
-v会删除数据库数据,生产环境慎用。
10.4 备份
bash
# 备份 MySQL
docker exec demo-mysql sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" demo' > backup_$(date +%Y%m%d).sql
# 备份 Redis(RDB 文件)
docker cp demo-redis:/data/dump.rdb ./redis_backup.rdb
# 恢复 MySQL
cat backup.sql | docker exec -i demo-mysql sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" demo'
11. 验证与排错
11.1 查看容器状态
bash
docker compose ps
所有服务应该是 running (healthy) 状态。
11.2 查看日志
bash
docker compose logs -f nginx
docker compose logs -f backend
11.3 常见问题速查
| 症状 | 原因 | 解决 |
|---|---|---|
| 后端连不上数据库 | 用了 localhost | 改成服务名 mysql |
| 初始化 SQL 没执行 | 数据卷已存在 | docker compose down -v 后重启 |
| Navicat 连不上 MySQL 8 | 认证插件不兼容 | 连接串加 allowPublicKeyRetrieval=true |
| 端口被占用 | 本机已有服务 | 改 .env 里的映射端口 |
| 前端页面 404 | SPA 路由没配 | Nginx 加 try_files 回退 |
12. 总结
到这里,一套完整的全栈开发环境就搭好了。回顾一下我们做了什么:
- 端口规划:只暴露 Nginx,容器间用内部网络通信;
- 初始化 SQL 注入:首次启动自动建表、插数据;
- 健康检查 :
depends_on配合condition: service_healthy保证启动顺序; - 热更新挂载:开发模式源码直挂,改完即生效;
- 双模式切换 :一套 compose 文件,
.env+ 覆盖文件切换开发/生产; - README 四操作:启动 / 停止 / 重置 / 备份,clone 即跑。
三个坑位也帮你提前踩平了:
- 端口规划冲突 → 统一在
.env管理,README 写清楚; - MySQL 8 认证插件兼容 → 连接串加
allowPublicKeyRetrieval=true; - 容器内 localhost 误区 → 一律用服务名通信。
现在,把这份工程推到 GitHub,任何人在任何机器上 git clone 加 docker compose up -d,三分钟就能跑起完整环境。这就是"clone 即跑"的最终形态。
系列到此收官。从第一行的 Docker 命令,到这一整套可复制的工程,你已经拥有了独立搭建和交付全栈环境的能力。剩下的,就是在真实项目里不断打磨了。