一套 Compose 搭起完整开发环境:MySQL + Redis + Nginx

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 很可能被占用。两个解决办法:

  1. 开发模式改映射端口,比如 3307:3306;
  2. 用 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. 总结

到这里,一套完整的全栈开发环境就搭好了。回顾一下我们做了什么:

  1. 端口规划:只暴露 Nginx,容器间用内部网络通信;
  2. 初始化 SQL 注入:首次启动自动建表、插数据;
  3. 健康检查 :depends_on 配合 condition: service_healthy 保证启动顺序;
  4. 热更新挂载:开发模式源码直挂,改完即生效;
  5. 双模式切换 :一套 compose 文件,.env + 覆盖文件切换开发/生产;
  6. README 四操作:启动 / 停止 / 重置 / 备份,clone 即跑。

三个坑位也帮你提前踩平了:

  • 端口规划冲突 → 统一在 .env 管理,README 写清楚;
  • MySQL 8 认证插件兼容 → 连接串加 allowPublicKeyRetrieval=true;
  • 容器内 localhost 误区 → 一律用服务名通信。

现在,把这份工程推到 GitHub,任何人在任何机器上 git clone 加 docker compose up -d,三分钟就能跑起完整环境。这就是"clone 即跑"的最终形态。

系列到此收官。从第一行的 Docker 命令,到这一整套可复制的工程,你已经拥有了独立搭建和交付全栈环境的能力。剩下的,就是在真实项目里不断打磨了。

相关推荐
xbzb1 小时前
Nginx 反向代理总 404?Location 匹配与 proxy_pass 尾斜杠避坑手册
linux·运维·nginx·虚拟主机
ShayneLee81 小时前
Nginx只开一个端口!能做什么?(一)
运维·nginx
江湖有缘2 小时前
Docker实战 | 使用Docker部署Bibliotheca阅读习惯管理工具
java·docker·容器
万联WANFLOW2 小时前
Docker Hub 镜像拉取慢、timeout 的排查方法
运维·docker·云原生·容器·eureka
天衍四九-2 小时前
Docker Compose企业实战系列(一):LNMP环境一键部署(Nginx\+MySQL\+PHP)
mysql·nginx·docker
大貔貅喝啤酒2 小时前
Gitea+Jenkins+Docker 搭建 Node 项目 CI/CD 自动部署完整教程
ci/cd·docker·jenkins·js·gitea
Mortalbreeze2 小时前
MySQL 基础篇(二):数据库和数据表的基本操作
linux·服务器·数据库·mysql
AlfredZhao2 小时前
用 Docker 简化部署与版本升级:一份可复用的实践
docker
zzj_26261011 小时前
MySQL常用操作
数据库·mysql