前端 Docker 开发与生产部署指南
本文档以
yudao-ui-admin-uniapp项目(Vue3 + uniapp + vite + pnpm)为例,系统讲解前端项目在 Docker 环境下的开发与生产部署。所有概念均从零讲起,无需 Docker 基础。但是作为前端开发环境使用,还是不推荐使用
docker,我遇到的问题是开发过程中卡在了实时更新这个问题上,导致最终回到了nvm的方式。
目录
- 一、Docker 核心概念
- 二、Docker Desktop 侧边栏菜单详解
- 三、镜像、容器、数据卷的空间占用与关系
- 四、.nvmrc 机制与本地方案
- 五、前端 Docker 开发流程(yudao-ui-admin-uniapp)
- 六、端口映射与网络访问原理
- 七、数据卷的共用与隔离原则
- 八、Node 服务通过环境变量配置存储路径
- 九、Compose 编排详解
- 十、一个 nginx 容器托管多个前端
- 十一、生产环境 Docker 配置
- 十二、Docker 最佳实践与避坑指南
- 十三、日常使用:图形界面操作与日志查看
- 十四、Docker 磁盘存储位置与空间管理
- 十五、快捷启动命令
- 十六、uniapp 多平台 Docker 开发与构建
- 十七、镜像与数据卷的导出导入(环境迁移)
- 十八、Docker 进阶话题
一、Docker 核心概念
Docker 是一种"轻量级虚拟化"技术,它把应用和依赖打包成一个标准化单元(容器),让应用能在任何环境里一致地运行。
1.1 三个核心对象
| 概念 | 通俗理解 | 类比 | 特性 |
|---|---|---|---|
| 镜像(Image) | 应用的"安装包",包含运行所需的所有文件 | 系统安装光盘 ISO | 只读、可分发、可重建 |
| 容器(Container) | 镜像跑起来的"活实例" | 装好正在运行的系统 | 可读写、可启停、可删除 |
| 数据卷(Volume) | Docker 管理的持久化存储 | 插在电脑上的 U 盘 | 独立于容器生命周期,删容器不删数据 |
1.2 三者关系
Dockerfile ──build──> Image(镜像:只读模板)
│
│ docker run / docker compose up
▼
Container(容器:活的进程)
│
│ 挂载(mount)
▼
Volume(数据卷:持久化数据)
1.3 与虚拟机的区别
| 维度 | 虚拟机 | Docker 容器 |
|---|---|---|
| 隔离级别 | 硬件级(每个 VM 有完整 OS) | 进程级(共享宿主机内核) |
| 启动速度 | 分钟级 | 秒级 |
| 资源占用 | GB 级 | MB 级 |
| 镜像大小 | 几 GB | 几十 MB ~ 几百 MB |
二、Docker Desktop 侧边栏菜单详解
Docker Desktop 是 Docker 在 Windows/Mac 上的图形界面。左侧菜单逐条说明:
| 菜单 | 是什么 | 通俗理解 |
|---|---|---|
| Containers | 容器列表 | 正在跑(或停着)的进程实例。你的 uniapp dev server 就在这里面 |
| Images | 镜像列表 | 容器的"安装包"。如 node:20-alpine、yudao-ui-admin-uniapp-uniapp-dev |
| Volumes | 数据卷列表 | Docker 管理的持久化存储。如 uniapp_node_modules 命名卷 |
| Builds | 构建历史 | 每次 docker build / docker compose up --build 的一条记录 |
| Docker Scout | 安全扫描 | 检查镜像里有没有漏洞依赖(高级功能,日常不常用) |
| Extensions | 扩展插件 | 类似 VSCode 插件,给 Docker Desktop 加功能 |
2.1 Images 页面的状态标识
| 状态 | 含义 | 操作建议 |
|---|---|---|
| In use | 正被某个容器引用 | 不能直接删,先停容器 |
| Unused | 没有容器使用 | 可以删,释放空间 |
2.2 Builds 历史
每次执行 docker build 或 docker compose up --build,Docker Desktop 会记一条构建历史。只在 build 时新增,普通 up 不会新增。失败的构建也会记录,可在 Builds 页面点进去看错误日志。
三、镜像、容器、数据卷的空间占用与关系
三者都占用磁盘空间,只是大小和生命周期不同。
3.1 用"装电脑"类比
镜像(Image) = 系统安装光盘(ISO) → 占空间最大
容器(Container)= 装好的系统,正在运行 → 在镜像之上加一层薄的可写层,占用小
数据卷(Volume) = U 盘,插电脑上存数据 → 大小看你存了多少东西
3.2 实际占用示例(yudao-ui-admin-uniapp 项目)
| 东西 | 类比 | 占用空间 | 删掉会怎样 |
|---|---|---|---|
yudao-ui-admin-uniapp-uniapp-dev 镜像 | uniapp 安装盘 | ~700 MB | 容器跑不了,需要重新 build |
uniapp-dev 容器(运行中) | 装好的 uniapp 系统 | 几 MB(可写层) | 服务停了,数据卷还在 |
uniapp_node_modules 数据卷 | 存 node_modules 的 U 盘 | 几百 MB | 依赖没了,下次启动要重装 |
3.3 为什么"删容器不会删镜像"
你电脑装了个游戏(镜像 ISO = 50GB)
运行起来玩(容器 = 游戏进程 + 存档)
存档放在 U 盘里(数据卷)
关机(删容器)→ 游戏还在(镜像还在)
拔 U 盘(删数据卷)→ 存档没了,但游戏还能重新运行
删安装盘(删镜像)→ 游戏彻底没了,需要重新下载
3.4 空间清理建议(按收益排序)
- 删 Unused 镜像:Images 页面 → 删 Unused 状态的镜像
- 删停止的容器:Containers 页面 → 删 Exited 状态的容器
- 删废弃的数据卷:Volumes 页面 → 删没被任何容器引用的卷
- 一键全清:Docker Desktop → Troubleshoot → Clean / Purge data(谨慎)
Docker 会自动回收未使用的镜像层(garbage collection),平时不用刻意清理,磁盘紧张时再手动清。
四、.nvmrc 机制与本地方案
4.1 什么是 .nvmrc
.nvmrc 是一个文本文件,内容只有一行,指定项目所需的 Node.js 版本:
20
也可以写精确版本(20.18.0)或 LTS 代号(lts/iron)。
4.2 工作机制
.nvmrc 本身不做事,它依赖 Node 版本管理器工具来读取:
| 工具 | 安装 | 自动读取 .nvmrc |
|---|---|---|
| nvm | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash | nvm use 时手动读 |
| fnm | curl -fsSL https://fnm.vercel.app/install | bash | 配合 shell hook,cd 进目录自动切换 |
| volta | curl https://get.volta.sh | bash | 自动读取 |
4.3 典型本地开发流程
# 1. 在项目根目录创建 .nvmrc
echo "20" > .nvmrc
# 2. 进入项目目录,切换 Node 版本
cd /path/to/project
nvm use # 读取 .nvmrc,切到 Node 20
# 3. 安装依赖、启动
pnpm install
pnpm dev
4.4 .nvmrc 与 Docker 的关系
两者无关,解决的是不同问题:
| 方案 | 解决的问题 | 机制 |
|---|---|---|
.nvmrc | 本机切 Node 版本 | 依赖 nvm/fnm 工具读文件 |
| Docker | 整个运行环境隔离 | FROM node:20-alpine 直接锁定版本 |
Docker 里 Node 版本由 Dockerfile 的 FROM 指令直接锁定,比 .nvmrc 更硬。本项目 package.json 里 engines.node >= 20,所以 Docker 用 node:20-alpine。
五、前端 Docker 开发流程(yudao-ui-admin-uniapp)
5.1 为什么前端要用 Docker 开发
| 痛点 | Docker 解决方案 |
|---|---|
| 团队成员 Node 版本不一致 | 镜像锁定版本,人人一样 |
| Windows 装某些 npm 包失败(如 node-sass) | 容器内是 Linux,原生兼容 |
| 新人入职装环境要半天 | docker compose up 一条命令搞定 |
| 切换项目时依赖冲突 | 每个项目独立容器,互不干扰 |
5.2 项目关键事实(影响 Docker 方案)
| 项 | 现状 | 对 Docker 的影响 |
|---|---|---|
| Node 版本 | engines.node >= 20,packageManager: pnpm@10.10.0 | 用 node:20-alpine + corepack |
| 平台二进制 | @esbuild/darwin-arm64、@esbuild/darwin-x64、@rollup/rollup-darwin-x64 写死 | 不能把宿主机 node_modules 挂进容器,必须容器内 pnpm i |
| dev server | host: '0.0.0.0', port: 9000, hmr: true | 已绑 0.0.0.0,端口映射即用 |
| 后端代理 | VITE_APP_PROXY_ENABLE=true,代理前缀 /yudao-server/admin-api | 需补 VITE_SERVER_TARGET 指向后端 |
| predev 钩子 | predev 会跑 create-base-files.js 生成 pages.json | 容器内 pnpm dev:h5 会自动触发 |
5.3 三个核心文件
5.3.1 Dockerfile.dev
FROM node:20-alpine
# git 被 husky 间接需要;corepack 启用 pnpm@10
RUN apk add --no-cache git && corepack enable
WORKDIR /app
# 只拷依赖描述文件,利用 docker 层缓存。源码通过 volume 挂载,不 COPY。
COPY package.json pnpm-lock.yaml .npmrc ./
# --ignore-scripts: 跳过 prepare 钩子(init-husky / init-baseFiles),
# 因为 scripts/ 目录此时还没拷进来。base 文件会在运行时由 predev 钩子生成。
RUN pnpm install --frozen-lockfile --ignore-scripts
EXPOSE 9000
# 默认 H5 开发模式,predev 钩子会自动生成 pages.json 等基础文件
CMD ["pnpm", "dev:h5"]
5.3.2 docker-compose.dev.yml
services:
uniapp-dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "9000:9000"
volumes:
# 源码挂进容器:IDE 改代码 → 容器内 vite HMR
- .:/app
# 隔离 Linux node_modules,避免被 Windows 宿主机二进制污染
- uniapp_node_modules:/app/node_modules
environment:
# 后端 yudao-server 地址:
# - 后端在宿主机 IDE 跑 → host.docker.internal
# - 后端在局域网某机器跑 → 直接写 IP,如 192.168.1.182
- VITE_SERVER_TARGET=http://host.docker.internal:48080
# 容器里没有微信开发者工具,跳过 openDevTools 插件
- SKIP_OPEN_DEVTOOLS=true
extra_hosts:
- "host.docker.internal:host-gateway"
stdin_open: true
tty: true
volumes:
uniapp_node_modules:
5.3.3 .dockerignore
node_modules
dist
.git
docs
.image
.husky
.vscode
.github
*.log
5.4 启动与使用
cd yudao-ui-admin-uniapp
docker compose -f docker-compose.dev.yml up
浏览器打开 http://localhost:9000/admin-ui-uniapp/。
5.5 工作流程
┌────────────────────────────────────────────────────────────┐
│ 宿主机(Windows) │
│ ┌─────────────────────┐ │
│ │ VSCode/IDE │ ← 你在这里改代码 │
│ │ yudao-ui-admin- │ │
│ │ uniapp/src/... │ │
│ └──────────┬──────────┘ │
│ │ 源码目录通过 volume 挂载 │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Docker 容器 │ │
│ │ - pnpm dev:h5 │ ← vite dev server 在这里跑 │
│ │ - vite (port 9000) │ │
│ │ - node_modules │ ← 命名卷,Linux 二进制 │
│ └──────────┬──────────┘ │
│ │ 端口映射 9000:9000 │
│ ▼ │
│ 浏览器访问 http://localhost:9000 │
└────────────────────────────────────────────────────────────┘
改代码 → 容器内 vite 监听文件变化 → HMR 推送更新到浏览器。整个过程对开发者透明,和本地开发体验一致。
5.6 常见问题
Q1:pnpm install 时报 MODULE_NOT_FOUND: create-base-files.js
原因:pnpm install 自动跑 prepare 脚本,但构建时只拷了 package.json,scripts/ 目录还没进去。
解决:加 --ignore-scripts 跳过 prepare 钩子。base 文件会在运行时由 predev 钩子生成(那时源码已挂载)。
Q2:拉镜像报 dialing registry-1.docker.io:443 超时
原因:国内访问 Docker Hub 不稳定。
解决:配 Docker 镜像加速。Docker Desktop → Settings → Docker Engine:
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com",
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com"
]
}
或换基础镜像源为阿里云:FROM registry.cn-hangzhou.aliyuncs.com/library/node:20-alpine。
Q3:HMR 不生效
可能原因:vite HMR websocket 连不上。
解决:在 docker-compose.dev.yml 的 environment 加:
- VITE_HMR_HOST=localhost
- VITE_HMR_PORT=9000
并在 vite.config.ts 的 server 配置加 hmr.host 和 hmr.clientPort。
六、端口映射与网络访问原理
6.1 为什么需要端口映射
容器有自己的网络(默认 bridge),宿主机无法直接访问容器端口。必须把宿主机端口映射到容器端口。
浏览器访问 localhost:9000
│
▼
宿主机 9000 端口
│ ports: "9000:9000"
▼
容器内 9000 端口(vite dev server 监听)
6.2 映射格式
ports:
- "9000:9000" # 宿主机端口:容器端口
- "8080:80" # 宿主机 8080 → 容器 80
- "127.0.0.1:9000:9000" # 只允许本机访问
6.3 vite 为什么需要 host: '0.0.0.0'
vite 默认只监听 localhost,容器内 localhost 只能容器自己访问。改为 0.0.0.0 表示监听所有网卡,端口映射才能把流量转进来。
本项目 vite.config.ts 已配 host: '0.0.0.0',无需改动。
6.4 容器访问宿主机服务(host.docker.internal)
容器内要访问宿主机跑的服务(如后端 IDE 跑的 yudao-server),用特殊域名 host.docker.internal:
environment:
- VITE_SERVER_TARGET=http://host.docker.internal:48080
extra_hosts:
- "host.docker.internal:host-gateway"
- Windows/Mac:Docker Desktop 自带
host.docker.internal - Linux:需要
extra_hosts配置才能用
6.5 容器间互访(服务名 DNS)
同一个 compose 网络里的容器,可以用服务名互相访问,Docker 自动建 DNS:
services:
mysql:
# ...
yudao-server:
environment:
# 用服务名 mysql,不是 IP
- SPRING_REDIS_HOST=redis
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/qs_crm
七、数据卷的共用与隔离原则
7.1 node_modules 数据卷不能多个项目共用
即使几个前端项目依赖看起来一样,也不要共享一个 node_modules 数据卷。原因:
| 问题 | 解释 |
|---|---|
| 并发写冲突 | 多个容器同时往同一个数据卷写,可能互相覆盖或锁死 |
| 隐形差异 | 表面依赖相同,实际 package-lock.yaml 锁定的精确版本、子依赖树结构可能不同 |
| 项目隔离 | A 项目升级了一个 patch 版本,B 项目不知情就崩了 |
正确做法:每个项目一个独立的数据卷,命名区分:
# 项目 A
volumes:
project_a_node_modules:
# 项目 B
volumes:
project_b_node_modules:
数据卷在磁盘上只存"差异层",底层公共镜像层是共享的,所以不浪费空间。
7.2 业务数据卷按需加
项目需要存文件(如用户上传),单独加一个数据卷:
volumes:
- uniapp_node_modules:/app/node_modules # 依赖隔离
- uniapp_uploads:/app/public/uploads # 上传文件持久化
7.3 命名卷 vs bind mount
| 类型 | 语法 | 特点 | 适用场景 |
|---|---|---|---|
| 命名卷(Named Volume) | uniapp_node_modules:/app/node_modules | Docker 管理,路径不透明 | 依赖缓存、数据库数据 |
| bind mount | ./uploads:/app/public/uploads | 映射宿主机目录,可直接查看 | 上传文件、日志、配置文件 |
关键原则:
node_modules必须用命名卷(跨平台二进制问题,宿主机和容器 OS 不同)- 业务数据两种都行,看是否需要宿主机直接访问
八、Node 服务通过环境变量配置存储路径
8.1 标准做法
Node.js 通过 process.env 读环境变量,Docker 通过 environment 注入,数据卷挂载到对应路径。三方解耦。
Node 代码
const fs = require('fs');
const path = require('path');
// 从环境变量读保存路径,没配置就默认 /app/uploads
const uploadDir = process.env.UPLOAD_DIR || '/app/uploads';
// 确保目录存在
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 保存文件
fs.writeFileSync(path.join(uploadDir, 'test.txt'), 'hello');
Docker Compose
services:
my-node-app:
build: .
environment:
- UPLOAD_DIR=/app/data/uploads
volumes:
- node_modules:/app/node_modules
- upload_data:/app/data/uploads # 数据卷路径与环境变量一致
volumes:
node_modules:
upload_data:
8.2 优势
| 优势 | 说明 |
|---|---|
| 环境隔离 | 开发/测试/生产用不同路径,代码不动 |
| 路径可配 | 不改代码,改 compose 即可 |
| 持久化 | 数据卷独立于容器,删容器数据还在 |
九、Compose 编排详解
9.1 什么是 Compose
一句话:把"几个容器怎么一起跑"写在一个 yaml 里,一条命令全起全停。
9.2 不用 Compose vs 用 Compose
不用 Compose(手动):
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=xxx ...一堆参数
docker run -d --name redis --requirepass xxx ...一堆参数
docker run -d --name server --link mysql --link redis ...一堆参数
docker run -d --name nginx -p 80:80 ...一堆参数
→ 容器间 IP 怎么互通?启动顺序谁先?挂了怎么重启?全靠你手动管
用 Compose(自动):
docker compose up -d
→ 一条命令,按依赖顺序起,自动建网络,自动重启,自动清理
9.3 Compose 文件结构
services: # 定义所有容器
服务名1:
image: ... # 用现成镜像
build: ... # 或从 Dockerfile 构建
ports: ... # 端口映射
volumes: ... # 数据卷挂载
environment: ... # 环境变量
depends_on: ... # 依赖关系(启动顺序)
restart: ... # 重启策略
volumes: # 定义数据卷
卷名:
networks: # 定义网络(可选,默认会建 bridge)
网络名:
9.4 关键字段说明
| 字段 | 作用 | 示例 |
|---|---|---|
image | 用现成镜像 | image: mysql:8.0 |
build | 从 Dockerfile 构建 | build: { context: ./server, dockerfile: Dockerfile } |
ports | 端口映射 | "80:80" = 宿主机 80 → 容器 80 |
volumes | 挂载 | mysql_data:/var/lib/mysql = 命名卷挂到容器路径 |
environment | 环境变量 | MYSQL_ROOT_PASSWORD=123456 |
depends_on | 依赖 | 等 mysql 起来再起 server |
restart | 重启策略 | always = 挂了自动重启 |
healthcheck | 健康检查 | 定期检查, unhealthy 会触发重启 |
container_name | 容器名 | qs-mysql(不指定则用 目录名-服务名-1) |
9.5 常用命令
docker compose up -d # 后台启动所有服务
docker compose up -d --build # 强制重新构建镜像
docker compose ps # 查看服务状态
docker compose logs -f 服务名 # 看日志
docker compose stop # 停止(不删容器)
docker compose down # 停止并删除容器/网络(保留数据卷)
docker compose down -v # 停止并删除容器+数据卷(谨慎!)
十、一个 nginx 容器托管多个前端
10.1 场景
有 3 个前端项目:
yudao-ui-admin-uniapp(uniapp H5)yudao-ui-admin-vue3(PC 后台)yudao-mall-h5(商城 H5)
不想每个都起一个容器,想用一个 nginx 容器统一托管。
10.2 原理
nginx 根据 URL 路径分流到不同静态目录:
浏览器请求 nginx 容器内
─────────────────────────────────────────────────────
/admin-uniapp/... ───► /usr/share/nginx/html/admin-uniapp/index.html
/admin-vue3/... ───► /usr/share/nginx/html/admin-vue3/index.html
/mall-h5/... ───► /usr/share/nginx/html/mall-h5/index.html
/yudao-server/... ───► proxy_pass http://yudao-server:48080
10.3 物理结构
┌─────────────── nginx 容器 ───────────────┐
│ /usr/share/nginx/html/ │
│ ├── admin-uniapp/ ← uniapp H5 产物 │
│ ├── admin-vue3/ ← vue3 后台产物 │
│ └── mall-h5/ ← 商城 H5 产物 │
│ │
│ /yudao-server/ → 反代到后端容器 │
└──────────────────────────────────────────┘
10.4 Compose 配置示例
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./frontend-a/dist:/usr/share/nginx/html/a
- ./frontend-b/dist:/usr/share/nginx/html/b
- ./frontend-c/dist:/usr/share/nginx/html/c
depends_on:
- yudao-server
10.5 nginx 配置示例
server {
listen 80;
# 前端 A
location /admin-uniapp/ {
root /usr/share/nginx/html;
try_files $uri $uri/ /admin-uniapp/index.html;
}
# 前端 B
location /admin-vue3/ {
root /usr/share/nginx/html;
try_files $uri $uri/ /admin-vue3/index.html;
}
# 后端 API 反向代理
location /yudao-server/ {
proxy_pass http://yudao-server:48080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
10.6 对比:每个前端一个容器
| 方案 | 优点 | 缺点 |
|---|---|---|
| 一个 nginx 托管多前端 | 省资源、配置集中 | 改一个前端要重建 nginx 镜像/重挂卷 |
| 每个前端一个容器 | 隔离性好、独立升级 | 资源占用多、需要外面再套一层反代 |
十一、生产环境 Docker 配置
11.1 开发 vs 生产 的差异
| 维度 | 开发 | 生产 |
|---|---|---|
| 前端服务 | vite dev server(带 HMR) | nginx 托管静态文件 |
| 后端服务 | IDE 里跑 | 容器里跑 jar |
| 数据库 | 连宿主机/局域网现有 | 容器里跑(数据卷持久化) |
| 源码挂载 | 是(HMR 需要) | 否(镜像里打包产物) |
| 自动重启 | 不需要 | restart: always |
| 日志 | 控制台看 | 挂卷到宿主机或日志中心 |
11.2 生产 Dockerfile(前端,多阶段构建)
# ===== 阶段 1:构建 H5 产物 =====
FROM node:20-alpine AS builder
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml .npmrc ./
RUN pnpm install --frozen-lockfile --ignore-scripts
COPY . .
# 触发 prebuild:h5 钩子生成 pages.json,然后构建
RUN pnpm build:h5
# ===== 阶段 2:nginx 运行 =====
FROM nginx:alpine
# 把构建产物复制到 nginx 的 /admin-ui-uniapp/ 子路径
COPY --from=builder /app/dist/build/h5 /usr/share/nginx/html/admin-ui-uniapp
# nginx 配置模板(用 envsubst 注入后端地址)
COPY nginx.conf /etc/nginx/templates/default.conf.template
EXPOSE 80
11.3 nginx 配置模板(生产)
server {
listen 80;
server_name _;
# gzip 压缩
gzip on;
gzip_types text/plain text/css application/javascript application/json;
# 前端静态文件(子路径部署)
location /admin-ui-uniapp/ {
alias /usr/share/nginx/html/admin-ui-uniapp/;
try_files $uri $uri/ /admin-ui-uniapp/index.html;
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
# 后端 API 反向代理(不做 rewrite,后端 prefix 已为 /yudao-server/admin-api)
location /yudao-server/ {
proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};
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;
}
}
envsubst 机制:nginx 官方镜像支持
/etc/nginx/templates/*.template文件,启动时自动用环境变量替换${VAR}。所以${BACKEND_HOST}在容器启动时通过-e BACKEND_HOST=xxx注入,无需重新构建镜像即可切换后端地址。
11.4 生产 docker-compose.yml(完整编排)
services:
# ──────────────── 数据库 ────────────────
mysql:
image: mysql:8.0
container_name: qs-mysql
restart: always
environment:
MYSQL_DATABASE: qs_crm
MYSQL_ROOT_PASSWORD: rongyi123$qwer
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init-sql:/docker-entrypoint-initdb.d # 首启自动导入 SQL
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# ──────────────── 缓存 ────────────────
redis:
image: redis:7-alpine
container_name: qs-redis
restart: always
command: redis-server --requirepass rongyi.com --appendonly yes
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "rongyi.com", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ──────────────── 后端 ────────────────
yudao-server:
build:
context: ./yudao-boot-mini/yudao-server
dockerfile: Dockerfile
container_name: qs-server
restart: always
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
environment:
JAVA_OPTS: "-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom"
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: jdbc:mysql://mysql:3306/qs_crm?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: root
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: rongyi123$qwer
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: "6379"
SPRING_REDIS_DATABASE: "3"
SPRING_REDIS_PASSWORD: rongyi.com
TZ: Asia/Shanghai
ports:
- "48080:48080"
# ──────────────── 前端(nginx + H5 静态文件)────────────────
uniapp-h5:
build:
context: ./yudao-ui-admin-uniapp
dockerfile: Dockerfile
container_name: qs-uniapp
restart: always
depends_on:
- yudao-server
environment:
BACKEND_HOST: yudao-server # 用服务名访问后端
BACKEND_PORT: "48080"
ports:
- "80:80"
volumes:
mysql_data:
redis_data:
11.5 生产部署流程
# 1. 后端打包 jar
cd yudao-boot-mini
mvn clean package -DskipTests
# 2. 数据库初始化 SQL 放到 init-sql/ 目录
mkdir -p init-sql
cp sql/mysql/ruoyi-vue-pro.sql init-sql/
cp sql/mysql/quartz.sql init-sql/
# 3. 一键启动
cd ..
docker compose up -d
# 4. 查看状态
docker compose ps
# 5. 看日志
docker compose logs -f yudao-server
11.6 为什么一容器一服务(不把前端后端塞一个容器)
不推荐塞一个容器里跑多个服务,原因:
| 问题 | 后果 |
|---|---|
| 一个挂了全挂 | 前端崩了,后端也跟着重启 |
| 资源不隔离 | 一个内存泄漏拖垮全家 |
| 升级困难 | 改一个组件要重建整个镜像 |
| 日志混乱 | 所有服务日志混在一起 |
| 横向扩展不行 | 想给后端加副本?做不到 |
推荐架构:一容器一服务,用 compose 编排。
十二、Docker 最佳实践与避坑指南
12.1 Dockerfile 最佳实践
| 实践 | 原因 |
|---|---|
| 用 alpine 基础镜像 | 体积小(~50MB vs ~900MB) |
| 多阶段构建 | 构建工具不进最终镜像 |
.dockerignore 排除无关文件 | 减小构建上下文 |
| 依赖描述文件单独 COPY + install | 利用层缓存,改代码不重装依赖 |
| 不用 root 用户运行 | 安全 |
| 合并 RUN 指令 | 减少镜像层数 |
12.2 Compose 最佳实践
| 实践 | 原因 |
|---|---|
| 用命名卷持久化数据 | 删容器不丢数据 |
depends_on + healthcheck | 确保依赖服务就绪 |
restart: always 生产必备 | 挂了自动重启 |
| 服务名互通而非 IP | Docker 自动 DNS,IP 会变 |
敏感信息用 .env 文件 | 不把密码写死在 compose |
12.3 前端 Docker 常见坑
| 坑 | 解决 |
|---|---|
| Windows 宿主机 node_modules 挂进容器报错 | 用命名卷隔离,不让宿主机覆盖 |
| vite HMR 不工作 | 检查 host: '0.0.0.0' 和端口映射 |
prepare 钩子构建失败 | 加 --ignore-scripts |
| 镜像拉不下来 | 配国内镜像加速 |
| 容器内访问 localhost 指向容器自己 | 用 host.docker.internal 访问宿主机 |
12.4 资源清理
# 删所有停止的容器
docker container prune
# 删所有 Unused 镜像
docker image prune -a
# 删所有未引用的数据卷
docker volume prune
# 一键清理(谨慎)
docker system prune -a
附录:本项目文件清单
| 文件 | 用途 | 阶段 |
|---|---|---|
yudao-ui-admin-uniapp/Dockerfile.dev | 开发用,跑 vite dev server | 开发 |
yudao-ui-admin-uniapp/docker-compose.dev.yml | 开发编排 | 开发 |
yudao-ui-admin-uniapp/Dockerfile.build | 构建用,含 Android SDK | 构建/开发 |
yudao-ui-admin-uniapp/docker-compose.build.yml | 构建编排 | 构建 |
yudao-ui-admin-uniapp/docker-compose.mp-dev.yml | 小程序/App 开发编排 | 开发 |
yudao-ui-admin-uniapp/.dockerignore | 排除无关文件 | 通用 |
yudao-ui-admin-uniapp/Dockerfile | 生产用,多阶段构建 + nginx | 生产 |
yudao-ui-admin-uniapp/nginx.conf | nginx 配置模板 | 生产 |
docker-compose.yml | 生产编排(mysql+redis+server+uniapp) | 生产 |
十三、日常使用:图形界面操作与日志查看
Docker Desktop 的图形界面足以覆盖日常 90% 的操作,命令行只在初始化、重建镜像时用。
13.1 一次创建,终身点击
容器首次创建后,就会一直在 Docker Desktop 的 Containers 列表里。日常启停只需点击按钮,不用敲命令。
第一次:初始化容器(只需做一次)
cd yudao-ui-admin-uniapp
docker compose -f docker-compose.dev.yml up -d
# 或: pnpm dev:docker
这步创建容器 uniapp-dev。之后它会常驻在 Containers 列表里。
日常使用:点击启停
打开 Docker Desktop → 左侧 Containers → 找到 uniapp-dev:
| 操作 | 方法 |
|---|---|
| 启动 | 点击容器右侧 ▶️ Start |
| 暂停 | 点击容器右侧 ⏸️ Stop |
| 重启 | 点击容器右侧 🔄 Restart |
底层等价于 docker start <name> / docker stop <name>。
13.2 up vs start 的关键区别
| 命令 | 作用 | 适用场景 |
|---|---|---|
docker compose up | 创建并启动容器 | 第一次初始化、或 down 删除后重建 |
docker compose down | 停止并删除容器 | 彻底清理 |
docker start <name> | 启动已存在的容器 | 日常开机后启动服务 |
docker stop <name> | 停止运行中的容器 | 临时暂停 |
日常用 Docker Desktop 点击 Start / Stop 即可,只有以下情况需要重新 up(重建):
| 场景 | 命令 |
|---|---|
修改了 Dockerfile.dev | pnpm dev:docker:rebuild |
修改了 docker-compose.dev.yml 的端口/挂载等配置 | pnpm dev:docker |
down 删除了容器想重开 | pnpm dev:docker |
仅仅是改代码 → 完全不用重启容器! vite HMR 会自动热更新。
13.3 图形界面查看日志
打开容器 → 切换到 Logs 标签页,即可看到实时日志。与命令行 docker logs -f 完全等价,显示的是同一份 stdout/stderr 输出。
| Docker Desktop 操作 | 等价命令行 |
|---|---|
| 打开容器 → Logs 标签页 | docker logs <容器名> |
| 切换 "Follow log"(▶️ 图标) | docker logs -f <容器名> |
| 滚动查看历史 | docker logs --tail <行数> |
Logs 页面技巧
| 操作 | 方法 |
|---|---|
| 实时跟踪 | 点右上角 ▶️ Follow log 图标(变成 ⏸️ 时代表暂停跟踪) |
| 清空当前显示 | 点 🗑️ Clear 图标(只是清屏,不影响后台日志) |
| 跳到最新 | 点 Jump to bottom 图标 |
| 筛选关键字 | 顶部搜索框输入,只显示匹配行 |
| 复制日志 | 选中文字 → Ctrl+C |
日常用 Docker Desktop 的 Logs 页看就好,不用开终端跑 docker compose logs -f。
十四、Docker 磁盘存储位置与空间管理
14.1 默认存储位置
Docker Desktop 在 Windows 上把所有镜像、容器、数据卷存在一个 WSL2 虚拟磁盘文件里:
C:\Users\<你的用户名>\AppData\Local\Docker\wsl\data\ext4.vhdx
所有镜像、容器、数据卷都打包在这一个 .vhdx 文件里,文件会随着使用增长,但不会自动收缩。
14.2 查看当前占用
方式一:Docker Desktop → 左下角 Troubleshoot → Clean / Purge data 旁边能看到当前磁盘使用量。
方式二:命令行
wsl -d docker-desktop -e df -h
方式三:查看 Docker 整体占用
docker system df
输出示例:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 3 2 1.2GB 350MB
Containers 2 1 50MB 20MB
Local Volumes 3 2 800MB 100MB
14.3 更改存储位置
当镜像太多、C 盘吃紧时,可以把 Docker 数据迁到其他盘。
步骤:
-
停止 Docker Desktop:右下角托盘右键 → Quit Docker Desktop
-
复制虚拟磁盘文件到新位置:
源: C:\Users\<用户名>\AppData\Local\Docker\wsl\data\ext4.vhdx 目标: D:\Docker\ext4.vhdx -
修改 Docker Desktop 设置:
- 启动 Docker Desktop
- Settings → Resources → Disk image location
- Browse 选择新路径(如
D:\Docker\) - Apply & Restart
重要:直接改路径不迁移数据的话,Docker 会重新创建空磁盘,之前所有镜像容器都没了。必须先复制
.vhdx文件到新位置。
14.4 限制磁盘大小
Settings → Resources → Disk image size → 拖动调整最大容量(默认通常 64GB)。
WSL2 的
.vhdx是动态扩展的,64GB 表示上限,实际占用看里面数据多少。
14.5 快速腾空间
改位置之前先清理无用数据,通常能省下几个 GB:
# 删所有停止的容器
docker container prune
# 删所有 Unused 镜像
docker image prune -a
# 删所有未引用的数据卷
docker volume prune
# 一键清理(删所有未使用的东西)
docker system prune -a
或图形界面:Docker Desktop → Images → 勾掉 Unused 的删除;Volumes → 删没被引用的卷。
14.6 压缩 vhdx 文件(进阶)
即使删了镜像,.vhdx 文件也不会自动缩小。要回收磁盘空间:
# 1. 关闭 Docker Desktop
# 2. 关闭 WSL
wsl --shutdown
# 3. 压缩磁盘
diskpart
# 在 diskpart 提示符里:
select vdisk file="D:\Docker\ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit
十五、快捷启动命令
为减少记忆负担,把常用 Docker 命令封装到 package.json 的 scripts 里,与项目现有 pnpm dev、pnpm build:mp 等风格一致。
15.1 命令列表
| 命令 | 作用 |
|---|---|
pnpm dev:docker | 启动 Docker 开发容器 |
pnpm dev:docker:stop | 停止开发容器 |
pnpm dev:docker:rebuild | 改了依赖/Dockerfile 后重建镜像并启动 |
pnpm dev:docker:logs | 查看容器日志 |
15.2 使用流程
# 第一次启动(创建容器 + 构建镜像 + 启动)
cd yudao-ui-admin-uniapp
pnpm dev:docker
# 日常使用:直接用 Docker Desktop 点 Start / Stop(见第十三章)
# 改了 package.json 依赖后
pnpm dev:docker:rebuild
# 临时停掉容器
pnpm dev:docker:stop
# 查看日志(与 Docker Desktop Logs 页等价)
pnpm dev:docker:logs
15.3 等价对照表
| 快捷命令 | 等价的 docker 命令 |
|---|---|
pnpm dev:docker | docker compose -f docker-compose.dev.yml up |
pnpm dev:docker:stop | docker compose -f docker-compose.dev.yml down |
pnpm dev:docker:rebuild | docker compose -f docker-compose.dev.yml up --build |
pnpm dev:docker:logs | docker compose -f docker-compose.dev.yml logs -f |
十六、uniapp 多平台 Docker 开发与构建
uniapp 支持 H5、小程序、App(Android/iOS)多端,所有开发和构建工作都可以放进 Docker。
16.1 两种开发模式
uniapp 的开发模式分两种,这是理解 Docker 开发的关键:
| 类型 | H5 | 小程序 / App |
|---|---|---|
| 本质 | dev server(vite 实时服务) | 编译产物 + watch(文件变了自动重编译) |
| 产物 | 无产物(内存中运行) | 有产物(dist/dev/ 目录) |
| 预览方式 | 浏览器直接访问 URL | 宿主机工具打开产物目录 |
| 热更新 | vite HMR(无需刷新) | 重新编译 + 刷新工具 |
16.2 H5 开发 vs 小程序/App 开发
H5 开发流程
Docker 容器内: pnpm dev:h5 (vite dev server, 端口 9000)
↓
宿主机端口映射 9000:9000
↓
浏览器访问 http://localhost:9000
↓
改代码 → vite HMR → 浏览器自动刷新
特点:容器内跑 dev server,浏览器直接连,无产物。
小程序开发流程
Docker 容器内: pnpm dev:mp-weixin (watch 模式持续编译)
↓ 产物映射
宿主机 dist/dev/mp-weixin/ ← 编译产物在这里
↓
宿主机微信开发者工具 → 导入这个目录
↓
改代码 → 容器内自动重编译 → 开发者工具自动刷新
特点:容器内跑 watch 编译,产物映射到宿主机,用宿主机的开发者工具预览。
App 开发流程(Android)
Docker 容器内: pnpm dev:app-android (watch 模式持续编译)
↓ 产物映射
宿主机 dist/dev/app/ ← 编译产物在这里
↓
宿主机 HBuilderX → 打开项目 → 运行到真机/模拟器
特点:同小程序,产物映射到宿主机,用宿主机工具预览。
16.3 各平台支持情况
| 平台 | Docker 开发 | Docker 构建 | 预览方式 |
|---|---|---|---|
| H5 | ✅ | ✅ | 浏览器访问 URL |
| 微信小程序 | ✅ | ✅ | 微信开发者工具(宿主机) |
| 支付宝等小程序 | ✅ | ✅ | 对应开发者工具(宿主机) |
| Android App | ✅ | ✅ | HBuilderX / Android Studio(宿主机) |
| iOS App | ❌ | ❌ | 必须 macOS |
16.4 开发快捷命令
H5 开发(已实现)
pnpm dev:docker # 启动 H5 dev server
pnpm dev:docker:stop # 停止
小程序/App 开发
pnpm dev:docker:mp-weixin # 启动微信小程序 watch 编译
pnpm dev:docker:mp-alipay # 启动支付宝小程序 watch 编译
pnpm dev:docker:app-android # 启动 Android App watch 编译
pnpm dev:docker:mp-stop # 停止所有
pnpm dev:docker:mp-logs # 查看日志
16.5 小程序/App 开发使用步骤
步骤 1:启动 watch 编译
cd yudao-ui-admin-uniapp
pnpm dev:docker:mp-weixin
步骤 2:用宿主机工具预览
- 微信小程序:打开微信开发者工具 → 导入项目 → 目录选择
dist/dev/mp-weixin - 支付宝小程序:打开支付宝小程序开发者工具 → 导入项目 → 目录选择
dist/dev/mp-alipay - Android App:打开 HBuilderX → 运行 → 运行到手机或模拟器
步骤 3:改代码自动刷新
在 VSCode 改代码保存后:
- Docker 容器内自动检测文件变化
- 自动重新编译
- 产物更新到
dist/dev/目录 - 开发者工具自动刷新(需在开发者工具中开启"自动编译")
16.6 构建快捷命令(生产构建)
pnpm build:docker:h5 # 构建 H5 → dist/docker/h5/
pnpm build:docker:mp-weixin # 构建微信小程序 → dist/docker/mp-weixin/
pnpm build:docker:app-android # 构建 Android App → dist/docker/app/
pnpm build:docker:shell # 进入容器 shell 手动构建
16.7 开发 vs 构建 对比
| 维度 | 开发 (dev:docker:xxx) | 构建 (build:docker:xxx) |
|---|---|---|
| 执行频率 | 启动一次,持续运行 | 需要构建时才跑 |
| 容器生命周期 | 常驻运行 | 一次性(构建完自动删) |
| 产物目录 | dist/dev/ | dist/docker/ |
| 改代码后 | 自动重编译 + 工具自动刷新 | 需要重新跑命令 |
| 核心命令 | pnpm dev:mp-weixin(watch 模式) | pnpm build:mp-weixin(单次构建) |
16.8 各平台开发差异总结
| 维度 | H5 | 小程序 | App |
|---|---|---|---|
| Docker 内跑什么 | vite dev server | uni watch 编译 | uni watch 编译 |
| 预览工具 | 浏览器 | 开发者工具(宿主机) | HBuilderX(宿主机) |
| 产物位置 | 无 | dist/dev/mp-xxx | dist/dev/app |
| 改代码刷新 | 浏览器自动 | 开发者工具自动 | HBuilderX 自动 |
| 端口映射 | 需要(9000) | 不需要 | 不需要 |
16.9 核心文件说明
Dockerfile.build(构建/开发共用镜像)
FROM node:20-alpine
# 基础工具 + Android SDK 依赖
RUN apk add --no-cache \
git \
curl \
unzip \
openjdk17 \
&& corepack enable
# 配置 Android SDK
ENV ANDROID_HOME=/opt/android-sdk
ENV ANDROID_SDK_ROOT=/opt/android-sdk
ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk
# 安装 Android SDK Command-line Tools
ARG ANDROID_CLI_VERSION=11076708
RUN mkdir -p $ANDROID_HOME/cmdline-tools && \
curl -L "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CLI_VERSION}_latest.zip" -o /tmp/android-cli.zip && \
unzip -q /tmp/android-cli.zip -d /tmp/ && \
mv /tmp/cmdline-tools $ANDROID_HOME/cmdline-tools/latest && \
rm -rf /tmp/*
# 接受 SDK 许可并安装必要组件
RUN yes | sdkmanager --licenses > /dev/null 2>&1 || true && \
sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" > /dev/null 2>&1 || true
WORKDIR /app
# 先拷贝依赖描述文件,利用层缓存
COPY package.json pnpm-lock.yaml .npmrc ./
RUN pnpm install --frozen-lockfile --ignore-scripts
# 默认命令:交互式 shell
CMD ["/bin/sh"]
关键设计:
- 基于
node:20-alpine,体积小 - 内置 Android SDK 34(Android App 开发/构建需要)
--ignore-scripts跳过 prepare 钩子(base 文件在 watch 启动时由 predev 生成)- 构建和开发共用同一个镜像,区别只在 compose 编排和启动命令
docker-compose.build.yml(生产构建编排)
services:
uniapp-build:
build:
context: .
dockerfile: Dockerfile.build
container_name: uniapp-build
volumes:
- .:/app
# 产物输出到宿主机 dist/docker/ 目录
- ./dist/docker/h5:/app/dist/build/h5
- ./dist/docker/mp-weixin:/app/dist/build/mp-weixin
- ./dist/docker/mp-alipay:/app/dist/build/mp-alipay
- ./dist/docker/app:/app/dist/build/app
- uniapp_build_node_modules:/app/node_modules
- android_sdk:/opt/android-sdk
environment:
- ANDROID_HOME=/opt/android-sdk
- SKIP_OPEN_DEVTOOLS=true
- WECHAT_DEVTOOLS_CLI_PATH=/bin/echo
command: /bin/sh
stdin_open: true
tty: true
volumes:
uniapp_build_node_modules:
android_sdk:
关键设计:
- 产物目录映射到
dist/docker/(生产构建产物) android_sdk命名卷持久化,避免重复下载 SDKuniapp_build_node_modules隔离 Linux 依赖- 配合
--rm使用,容器一次性
docker-compose.mp-dev.yml(小程序/App 开发编排)
services:
uniapp-mp-dev:
build:
context: .
dockerfile: Dockerfile.build
container_name: uniapp-mp-dev
volumes:
- .:/app
# 产物目录映射到宿主机 dist/dev/(开发产物)
- ./dist/dev/mp-weixin:/app/dist/dev/mp-weixin
- ./dist/dev/mp-alipay:/app/dist/dev/mp-alipay
- ./dist/dev/app:/app/dist/dev/app
- uniapp_mp_node_modules:/app/node_modules
- android_sdk:/opt/android-sdk
environment:
- SKIP_OPEN_DEVTOOLS=true
- WECHAT_DEVTOOLS_CLI_PATH=/bin/echo
- ANDROID_HOME=/opt/android-sdk
command: /bin/sh
stdin_open: true
tty: true
volumes:
uniapp_mp_node_modules:
android_sdk:
关键设计:
- 产物目录映射到
dist/dev/(开发产物,区别于构建的dist/docker/) - 常驻运行(不配
--rm),容器跑 watch 编译 - 复用
Dockerfile.build镜像(已含 Android SDK) node_modules用独立命名卷,与 build 容器隔离
三套 compose 文件对比
| 文件 | 用途 | 产物目录 | 容器生命周期 | 端口 |
|---|---|---|---|---|
docker-compose.dev.yml | H5 开发 | 无(dev server) | 常驻 | 9000:9000 |
docker-compose.mp-dev.yml | 小程序/App 开发 | dist/dev/ | 常驻 | 无 |
docker-compose.build.yml | 生产构建 | dist/docker/ | 一次性(--rm) | 无 |
三个 compose 共用两个镜像:
Dockerfile.dev(仅 H5 开发,轻量)和Dockerfile.build(含 Android SDK,H5/小程序/App 通用)。
十七、镜像与数据卷的导出导入(环境迁移)
镜像和数据卷是两种不同的对象,导出导入方式不同。镜像有专用命令,数据卷需要借临时容器打包。
17.1 镜像导出/导入
用 docker save / docker load,把镜像打包成 .tar 文件。
导出
# 查看镜像名
docker images
# 导出(单个镜像)
docker save -o yudao-uniapp-dev.tar yudao-ui-admin-uniapp-uniapp-dev:latest
# 导出(多个镜像合并成一个文件)
docker save -o all-images.tar yudao-ui-admin-uniapp-uniapp-dev:latest mysql:8.0 redis:7-alpine
参数说明:
-o输出文件名- 后面跟镜像名(REPOSITORY:TAG 或 IMAGE ID)
导入(另一台机器)
docker load -i yudao-uniapp-dev.tar
压缩导出(节省空间)
docker save 默认不压缩,可用 gzip 手动压:
# 导出并压缩
docker save yudao-ui-admin-uniapp-uniapp-dev:latest | gzip > yudao-uniapp-dev.tar.gz
# 导入(自动解压)
docker load -i yudao-uniapp-dev.tar.gz
17.2 数据卷导出/导入
数据卷没有直接的导出命令,要用临时容器 + tar 打包。
导出
# 查看数据卷
docker volume ls
# 用临时 alpine 容器把数据卷内容打包成 tar
docker run --rm -v uniapp_node_modules:/data -v ${PWD}:/backup alpine \
tar czf /backup/uniapp_node_modules.tar.gz -C /data .
命令拆解:
| 部分 | 作用 |
|---|---|
--rm | 容器执行完自动删除 |
-v uniapp_node_modules:/data | 把要导出的数据卷挂到 /data |
-v ${PWD}:/backup | 把当前目录挂到 /backup(用于输出文件) |
tar czf /backup/xxx.tar.gz -C /data . | 把 /data 内容打包到 /backup/xxx.tar.gz |
Windows PowerShell 用
${PWD},CMD 用%cd%。
导入(另一台机器)
# 先创建空数据卷
docker volume create uniapp_node_modules
# 用临时容器把 tar 解压进数据卷
docker run --rm -v uniapp_node_modules:/data -v ${PWD}:/backup alpine \
sh -c "cd /data && tar xzf /backup/uniapp_node_modules.tar.gz"
验证
# 用临时容器看数据卷内容
docker run --rm -v uniapp_node_modules:/data alpine ls -la /data
17.3 完整迁移示例(yudao 项目)
场景:把开发环境从 A 机迁到 B 机。
A 机:导出
cd e:\workspace-qisheng\CRM\yudao-ui-admin-uniapp
# 1. 导出镜像
docker save -o yudao-uniapp-dev.tar yudao-ui-admin-uniapp-uniapp-dev:latest
# 2. 导出数据卷(node_modules)
docker run --rm -v uniapp_node_modules:/data -v ${PWD}:/backup alpine \
tar czf /backup/uniapp_node_modules.tar.gz -C /data .
# 现在目录下有两个文件:
# yudao-uniapp-dev.tar (镜像,~700MB)
# uniapp_node_modules.tar.gz (依赖,~300MB)
B 机:导入
cd /path/to/yudao-ui-admin-uniapp
# 1. 导入镜像
docker load -i yudao-uniapp-dev.tar
# 2. 导入数据卷
docker volume create uniapp_node_modules
docker run --rm -v uniapp_node_modules:/data -v ${PWD}:/backup alpine \
sh -c "cd /data && tar xzf /backup/uniapp_node_modules.tar.gz"
# 3. 直接启动(不用 build,镜像已导入)
docker compose -f docker-compose.dev.yml up
17.4 三种对象导出对比
| 对象 | 导出命令 | 导入命令 | 文件格式 | 说明 |
|---|---|---|---|---|
| 镜像 | docker save -o xxx.tar 镜像名 | docker load -i xxx.tar | .tar(未压缩) | 含历史层,可重建容器 |
| 数据卷 | 临时容器 + tar czf | 临时容器 + tar xzf | .tar.gz(压缩) | 独立于容器,需借临时容器打包 |
| 容器 | docker export -o xxx.tar 容器名 | docker import xxx.tar | .tar(扁平文件系统) | 丢历史层,迁移不推荐用 |
迁移环境用
docker save(镜像)+ 临时容器 tar(数据卷),不要用docker export。
17.5 注意事项
| 注意点 | 说明 |
|---|---|
| 镜像 vs 容器 | docker save 导出镜像(含历史层),docker export 导出容器(扁平文件系统,丢历史)。迁移用 save |
| 数据卷不能直接 save | 数据卷独立于镜像,必须用临时容器打包 |
| 跨平台 | Linux 镜像不能在 Mac/Windows 跑(除非 ARM/x86 匹配)。但 Windows→Linux 服务器通常 OK(都是 x86) |
| 压缩 | docker save 不压缩,可用 gzip 手动压:docker save 镜像名 | gzip > xxx.tar.gz |
| 文件大小 | 镜像几百 MB,数据卷看内容,整体可能 1GB+,传文件注意带宽 |
| node_modules 跨平台 | Linux 容器的 node_modules 不能给 Windows 宿主机用(esbuild 等二进制不同),只能给另一台 Linux 容器 |
17.6 一键备份脚本
如果要经常备份,可以做个脚本:
#!/bin/bash
# backup-docker.sh
# 用法: bash backup-docker.sh <镜像名> <数据卷名>
IMAGE_NAME=$1
VOLUME_NAME=$2
BACKUP_DIR=./backup
mkdir -p $BACKUP_DIR
# 导出镜像
echo "导出镜像 $IMAGE_NAME..."
docker save -o $BACKUP_DIR/image.tar $IMAGE_NAME
# 导出数据卷
if [ -n "$VOLUME_NAME" ]; then
echo "导出数据卷 $VOLUME_NAME..."
docker run --rm -v $VOLUME_NAME:/data -v $BACKUP_DIR:/backup alpine \
tar czf /backup/volume.tar.gz -C /data .
fi
echo "完成!文件在 $BACKUP_DIR/"
使用:
bash backup-docker.sh yudao-ui-admin-uniapp-uniapp-dev:latest uniapp_node_modules
十八、Docker 进阶话题
本章覆盖生产部署、运维中常用的进阶配置。按实用度排序,前 6 节强烈建议掌握。
18.1 资源限制(CPU / 内存)
容器默认不限制资源,一个内存泄漏能拖垮整台机器。生产必配。
单服务配置
services:
yudao-server:
deploy:
resources:
limits:
cpus: '2' # 最多 2 核
memory: 1G # 最多 1GB 内存
reservations:
memory: 512M # 保底 512MB
查看实时占用
docker stats
# 输出示例
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O
qs-server 12.5% 480MiB / 1GiB 46.8% 12MB/8MB
qs-mysql 5.2% 320MiB / 2GiB 15.6% 4MB/2MB
资源限制策略
| 服务 | CPU 限制 | 内存限制 | 说明 |
|---|---|---|---|
| yudao-server | 2 核 | 1-2 GB | JVM 吃内存,给足 |
| mysql | 1-2 核 | 1-2 GB | 数据库缓存需要内存 |
| redis | 0.5 核 | 512 MB | Redis 本身很省 |
| nginx (前端) | 0.5 核 | 128 MB | 静态文件服务很轻 |
18.2 日志管理(防止日志撑爆磁盘)
容器日志默认无限增长,时间一长磁盘就满。生产必配日志轮转。
全局配置(推荐)
Docker Desktop → Settings → Docker Engine:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
单服务配置
services:
yudao-server:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
参数说明
| 参数 | 作用 | 推荐值 |
|---|---|---|
max-size | 单个日志文件最大 | 10m |
max-file | 最多保留几个文件 | 3 |
一个服务最多占用
max-size × max-file的磁盘空间(如 30MB)。
查看容器日志
# 看最近 100 行
docker logs --tail 100 uniapp-dev
# 实时跟踪
docker logs -f uniapp-dev
# 看指定时间段
docker logs --since 30m uniapp-dev
docker logs --since 2026-07-31T10:00:00 uniapp-dev
18.3 重启策略详解
restart: no # 默认,不重启
restart: always # 总是重启(包括手动 stop 后 daemon 重启也会拉起)
restart: unless-stopped # 推荐:除非手动 stop,否则总是重启
restart: on-failure # 只在异常退出时重启
restart: on-failure:5 # 异常退出重启,最多重试 5 次
对比
| 策略 | 容器挂了 | 手动 stop | Docker 重启 | 适合场景 |
|---|---|---|---|---|
no | 不重启 | - | 不拉起 | 开发 |
always | 重启 | - | 拉起 | 不推荐(手动停了还起) |
unless-stopped | 重启 | 不拉起 | 拉起(除非手动停过) | 生产推荐 |
on-failure | 重启 | - | 不拉起 | 任务型容器 |
生产推荐 unless-stopped:你手动停的不会自己起,挂了的会自动拉起。
18.4 镜像仓库(生产部署必备)
本地构建的镜像要推到服务器,不能靠 U 盘拷 tar。
镜像仓库选项
| 选项 | 类型 | 适用场景 | 费用 |
|---|---|---|---|
| Docker Hub | 公有 | 个人项目 | 免费(公开) |
| 阿里云容器镜像服务 ACR | 公有 | 国内生产 | 有免费额度 |
| 腾讯云 TCR | 公有 | 国内生产 | 有免费额度 |
| Harbor | 私有自建 | 企业内网 | 开源免费 |
推送流程
# 1. 登录镜像仓库
docker login registry.cn-hangzhou.aliyuncs.com
# 输入用户名密码
# 2. 给镜像打 tag(必须带仓库地址前缀)
docker tag yudao-ui-admin-uniapp:latest \
registry.cn-hangzhou.aliyuncs.com/your-namespace/yudao-uniapp:latest
# 3. 推送
docker push registry.cn-hangzhou.aliyuncs.com/your-namespace/yudao-uniapp:latest
# 4. 服务器拉取
docker pull registry.cn-hangzhou.aliyuncs.com/your-namespace/yudao-uniapp:latest
# 5. 服务器上跑(不用 build)
docker run -d -p 80:80 \
-e BACKEND_HOST=yudao-server \
registry.cn-hangzhou.aliyuncs.com/your-namespace/yudao-uniapp:latest
compose 用镜像仓库
services:
uniapp-h5:
image: registry.cn-hangzhou.aliyuncs.com/your-namespace/yudao-uniapp:latest
# 不用 build,直接用 image
18.5 多环境配置(compose override)
dev/test/prod 用不同配置,不用维护多份 compose。
文件结构
docker-compose.yml # 基础配置
docker-compose.override.yml # 默认覆盖(开发用,自动加载)
docker-compose.prod.yml # 生产覆盖
docker-compose.test.yml # 测试覆盖
示例
基础配置 docker-compose.yml:
services:
yudao-server:
image: yudao-server:latest
environment:
SPRING_REDIS_HOST: redis
生产覆盖 docker-compose.prod.yml:
services:
yudao-server:
restart: always
deploy:
resources:
limits:
memory: 2G
environment:
SPRING_REDIS_HOST: redis
SPRING_PROFILES_ACTIVE: prod
启动命令
# 开发:自动用 base + override
docker compose up -d
# 生产:指定 -f
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# 测试
docker compose -f docker-compose.yml -f docker-compose.test.yml up -d
18.6 调试技巧
常用调试命令
# 进入运行中的容器
docker exec -it uniapp-dev sh
# 查看容器详情(IP、挂载、环境变量)
docker inspect uniapp-dev
# 查看容器资源占用
docker stats uniapp-dev
# 查看容器进程
docker top uniapp-dev
# 查看容器端口映射
docker port uniapp-dev
# 查看容器文件系统变更
docker diff uniapp-dev
网络调试
# 看容器能不能连后端
docker exec uniapp-dev wget -qO- http://host.docker.internal:48080
# 看容器 DNS 解析
docker exec uniapp-dev nslookup mysql
# 看容器网络
docker network inspect yudao-ui-admin-uniapp_default
容器内调试工具
alpine 镜像很精简,需要手动装调试工具:
# 进入容器
docker exec -it uniapp-dev sh
# 装常用工具
apk add curl wget vim procps bind-tools
18.7 敏感信息管理
不要把密码写死在 compose,用 .env 文件。
.env 文件(git ignore)
# .env
MYSQL_ROOT_PASSWORD=rongyi123$qwer
REDIS_PASSWORD=rongyi.com
JWT_SECRET=your-jwt-secret
compose 引用
services:
mysql:
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
redis:
command: redis-server --requirepass ${REDIS_PASSWORD}
.gitignore 排除
.env
.env.local
.env.prod
启动时指定
# 默认读 .env
docker compose up
# 指定其他 env 文件
docker compose --env-file .env.prod up
18.8 多架构构建(buildx)
要同时支持 x86(服务器)和 ARM(M 系列 Mac / 树莓派)。
创建 builder
docker buildx create --use --name mybuilder
docker buildx inspect --bootstrap
构建多架构镜像
docker buildx build --platform linux/amd64,linux/arm64 \
-t myapp:latest --push .
多架构镜像必须推到镜像仓库,不能本地
docker run。
18.9 CI/CD 集成(GitHub Actions)
推送代码自动构建镜像并推到仓库。
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
- uses: docker/build-push-action@v5
with:
context: ./yudao-ui-admin-uniapp
push: true
tags: |
registry.cn-hangzhou.aliyuncs.com/ns/yudao-uniapp:latest
registry.cn-hangzhou.aliyuncs.com/ns/yudao-uniapp:${{ github.sha }}
18.10 健康检查自定义
只配 ping 不够,要检查业务是否真起来。
yudao-server:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:48080/actuator/health"]
interval: 30s # 每 30s 检查一次
timeout: 10s # 超时时间
retries: 3 # 连续失败 3 次算 unhealthy
start_period: 60s # 启动后给 60s 宽限期
参数说明
| 参数 | 作用 | 推荐值 |
|---|---|---|
test | 检查命令 | curl 业务接口 |
interval | 检查间隔 | 30s |
timeout | 单次超时 | 10s |
retries | 失败重试次数 | 3 |
start_period | 启动宽限期 | 60s(Java 应用启动慢) |
健康检查配合 depends_on
uniapp-h5:
depends_on:
yudao-server:
condition: service_healthy # 等后端健康再启动前端
18.11 Docker 网络
网络驱动
| 驱动 | 用途 | 说明 |
|---|---|---|
| bridge(默认) | 单机容器互访 | 最常用 |
| host | 容器直接用宿主机网络 | 无网络隔离 |
| overlay | 多机容器互访 | Swarm/K8s 用 |
| none | 无网络 | 安全隔离 |
常用命令
# 查看所有网络
docker network ls
# 创建自定义网络
docker network create mynet
# 查看网络详情
docker network inspect mynet
# 删除网络
docker network rm mynet
自定义网络示例
services:
yudao-server:
networks:
- backend
uniapp-h5:
networks:
- backend
- frontend
networks:
backend:
driver: bridge
frontend:
driver: bridge
自定义网络相比默认网络的优势:DNS 解析更快,隔离性更好。
18.12 从 Docker Compose 到 Kubernetes
Compose 适合单机,集群要上 K8s。
何时该上 K8s
| 场景 | 推荐 |
|---|---|
| 单机开发 | Docker Compose |
| 单机生产 | Docker Compose(够用) |
| 多机集群、高可用 | Kubernetes |
| 团队小、机器少 | Docker Compose + Nginx 负载均衡 |
kompose 转换工具
# 安装
curl -L https://github.com/kubernetes/kompose/releases/download/v1.31.2/kompose-linux-amd64 -o kompose
chmod +x kompose && sudo mv kompose /usr/local/bin/
# 转换
kompose convert -f docker-compose.yml
# 生成 K8s 的 yaml 文件(Deployment、Service 等)
18.13 学习路径建议
按你的情况,建议按以下顺序学习:
立即掌握(生产必备)
- 资源限制(18.1)+ 日志轮转(18.2)→ 否则早晚出事
- 重启策略(18.3)→
unless-stopped - 镜像仓库(18.4)→ 部署到服务器要用
- 多环境配置(18.5)→ dev/prod 切换
- 敏感信息管理(18.7)→ 不能把密码写代码里
用到再学
- 调试技巧(18.6)
- 健康检查自定义(18.10)
- Docker 网络(18.11)
进阶(一般用不上)
- 多架构构建(18.8)
- CI/CD 集成(18.9)
- K8s 迁移(18.12)
不用学的
- Docker Swarm:K8s 的对手,基本凉了
- Docker Storage Drivers:运维才关心
- Docker daemon 深度配置:除非做运维
- Docker plugins:很少用
第十九章 前端 Docker 开发性能问题排查与实践
本章重点解决 Windows 环境下 Docker 跑前端 dev server 时,文件修改无法实时同步(HMR 失效)的问题,以及 nvm-windows 的使用注意事项。内容包括问题根因分析、多种解决方案的利弊对比,以及最终推荐的本地开发方案。
19.1 问题描述
在 Windows 上使用 Docker 跑前端开发服务器(Vite + uniapp H5),修改代码后浏览器刷新页面看不到任何变化,必须重启 Docker 容器才能生效。Vite 的 HMR(Hot Module Replacement)完全失效,开发体验极差。
典型场景:
1. 在 VSCode 中修改组件文字(如删除"待办任务"前面的数字"1")
2. 保存文件
3. 刷新浏览器 → 页面没有任何变化
4. 重启 Docker 容器 → 修改生效
5. 每次改代码都要重启容器,开发效率极低
19.2 根因分析
Docker Desktop on Windows 使用 WSL2(Windows Subsystem for Linux 2)或 Hyper-V 虚拟化技术在宿主机和容器之间共享文件。Linux 内核的文件系统事件通知机制 inotify 无法穿透这个文件共享层。
宿主机(Windows)
│
│ VSCode 修改文件
│
▼
Docker 文件共享层(WSL2 / Hyper-V)
│
│ ⚠️ inotify 事件在这里丢失,无法传递到容器
│
▼
容器内 Linux 文件系统
│
│ chokidar(Vite 底层文件监听库)收不到事件
│
▼
Vite HMR 不触发,浏览器无变化
这是 Docker on Windows 的架构限制,不是配置问题。无论是 WSL2 还是 Hyper-V 模式,文件共享层都不支持 inotify 事件的透传。Mac 上的 Docker Desktop 也存在类似问题(通过 gRPC FUSE 共享文件系统)。
为什么 Linux 上没有这个问题
Linux 上的 Docker 容器直接运行在宿主机内核上,文件系统不需要经过虚拟化共享层。容器内的 inotify 能直接监听到宿主机文件变化,所以 HMR 正常工作。
为什么 macOS 上也有类似问题
macOS 上的 Docker Desktop 通过 gRPC FUSE 或 VirtioFS 共享文件,同样存在性能损失和事件丢失问题,但比 Windows 上的 WSL2 略好。
19.3 尝试过的方案及问题
19.3.1 全局轮询 CHOKIDAR_USEPOLLING
做法:在 docker-compose.dev.yml 的环境变量中添加 CHOKIDAR_USEPOLLING,强制 chokidar 使用轮询模式代替 inotify:
environment:
- CHOKIDAR_USEPOLLING=1
- CHOKIDAR_INTERVAL=300
原理:chokidar 是 Vite 底层使用的文件监听库。CHOKIDAR_USEPOLLING=1 让它不再依赖 inotify 事件,而是每隔 CHOKIDAR_INTERVAL 毫秒主动检查文件是否有变化。
效果:HMR 能工作了,但性能极差。
问题分析:
CHOKIDAR_USEPOLLING=1
│
▼
chokidar 轮询整个 /app 目录
│
├── src/ ← 源码文件(~1000 个)
├── node_modules/ ← 数万依赖文件 ← 性能杀手
├── .git/ ← git 对象文件
├── dist/ ← 构建产物
└── ...
│
▼
CPU 占用率飙升(可达 80%~100%)
│
▼
连 API 接口请求都变慢(因为 Vite 的进程被轮询占满)
结论:CHOKIDAR_USEPOLLING=1 会对整个 /app 目录(包括 node_modules 中数万个文件)做轮询,CPU 和 IO 被完全占满,导致整个开发环境变慢。不可用。
19.3.2 Vite server.watch 精确轮询
做法:在 vite.config.ts 中添加 server.watch 配置,只对源码目录开启轮询,排除 node_modules 等大目录:
server: {
watch: process.env.VITE_WATCH_POLLING === 'true'
? {
usePolling: true,
interval: 1000, // 1 秒检查一次
binaryInterval: 2000,
ignored: [ // 排除无关目录
'**/node_modules/**',
'**/.git/**',
'**/dist/**',
'**/unpackage/**',
'**/.vite/**',
'**/src/types/**',
],
}
: undefined,
},
效果:HMR 能工作,性能有所改善,但仍然有明显卡顿。
为什么还是慢:
1. 轮询间隔 1000ms → HMR 延迟至少 1 秒,响应迟钝
2. 即使只轮询 src/ 目录,Windows 文件共享层 + 1 秒间隔的组合
仍然导致 CPU 占用明显偏高
3. 轮询间隔调小(如 300ms)→ CPU 占用飙升
4. 轮询间隔调大(如 2000ms)→ HMR 延迟太长,体验差
结论:精确轮询能缓解问题,但无法从根本上解决。Docker on Windows 的文件共享层本质上是性能瓶颈,轮询方式再怎么优化也有天花板。
19.4 最终推荐方案:前端本地跑 + Docker 管基础设施
19.4.1 方案对比
核心思路:不把前端 dev server 放进 Docker,而是直接在 Windows 上原生运行。Docker 只负责基础设施(MySQL、Redis、后端服务)。
| 方案 | HMR 延迟 | CPU 占用 | 开发体验 |
|---|---|---|---|
| Docker + inotify(默认) | ❌ 不生效 | 无 | 改代码需重启容器 |
| Docker + 全局 polling | ~300ms | 🔥 80%~100% | 卡顿,接口都慢 |
| Docker + 精确 polling | ~1s | ⚡ 明显偏高 | 勉强可用,体验差 |
| 本地跑 + Docker 管基础设施 | <100ms | ✅ 正常 | 和原生开发一样 |
为什么本地跑更好:
Windows 原生开发环境:
VSCode 修改文件
│
▼
Windows 原生文件系统(NTFS)
│
▼
Vite(chokidar)直接监听
│ ReadDirectoryChangesW(Windows 原生 API)
▼
HMR 秒级触发(<100ms)
19.4.2 具体操作步骤
第一步:Docker 只启动基础设施
在项目根目录执行:
# 只启动 MySQL + Redis,不启动前端
docker compose up -d mysql redis
如果你用 Docker 跑后端,也可以一并启动:
docker compose up -d mysql redis yudao-server
第二步:确认前端代理配置
检查 yudao-ui-admin-uniapp/env/.env 文件,确保 VITE_SERVER_TARGET 指向本地后端地址:
# 后端代理目标(Vite 开发服务器会把 API 请求转发到这里)
VITE_SERVER_TARGET = 'http://127.0.0.1:48080'
如果后端在 Docker 容器里运行,
VITE_SERVER_TARGET应指向 Docker 暴露的宿主机端口,仍然是http://127.0.0.1:48080。
第三步:创建 .nvmrc 统一 Node 版本
在 yudao-ui-admin-uniapp/ 目录下创建 .nvmrc 文件:
20
用
20表示 Node.js 20 大版本,nvm 会自动选用最新的 20.x LTS。也可以写精确版本如20.18.0。
第四步:本地安装依赖并启动
cd yudao-ui-admin-uniapp
# 切换 Node 版本(确保已安装 Node 20)
nvm use 20
# 安装依赖(首次或依赖变更时)
pnpm install
# 启动 H5 开发服务器
pnpm dev:h5
启动后访问 http://localhost:9000/admin-ui-uniapp/。
18.4.3 日常开发流程
# 每天开机
docker compose up -d mysql redis # 启动基础设施
cd yudao-ui-admin-uniapp
pnpm dev:h5 # 启动前端
# 改代码 → 浏览器即时 HMR,无需任何手动操作
# 下班关机
# Ctrl+C 停掉前端
docker compose stop # 停掉基础设施
19.5 Windows 下 nvm 使用问题
19.5.1 问题描述
项目根目录已创建 .nvmrc 文件(内容为 20),但在 Windows PowerShell 中执行 nvm use 时,报错:
A version argument is required.
19.5.2 原因分析
Windows 上使用的是 nvm-windows(github.com/coreybutler…),它与 Linux/Mac 上的 nvm-sh(github.com/nvm-sh/nvm)是两个完全不同的项目,API 和行为不一致:
| 维度 | nvm-sh(Linux/Mac) | nvm-windows(Windows) |
|---|---|---|
| 开发语言 | Shell 脚本 | Go 语言 |
| 安装方式 | curl 脚本 | Windows 安装程序 |
.nvmrc 自动读取 | ✅ nvm use 自动读取 | ❌ 不支持 |
nvm use 命令 | 不传参数时读 .nvmrc | 必须传版本号参数 |
| 配置文件 | Shell 脚本加载 | 环境变量 NVM_HOME、NVM_SYMLINK |
关键差异:nvm-windows 的 nvm use 命令必须手动传入版本号,不会读取当前目录下的 .nvmrc 文件。
19.6 总结与最佳实践
核心结论
| 事项 | 结论 |
|---|---|
| Windows 上 Docker 跑前端 dev server | ❌ 不推荐,文件同步性能差,HMR 有问题 |
| 前端本地开发 | ✅ 推荐,原生文件监听,HMR 秒级响应 |
| Docker 的职责 | 基础设施(MySQL、Redis)、后端服务、生产构建 |
| Node 版本管理 | 用 .nvmrc + nvm/fnm/volta 管理,不依赖 Docker |
推荐开发环境架构
┌──────────────────────────────────────┐
│ Windows 宿主机 │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 前端开发环境 │ │
│ │ ├── VSCode + 源码 │ │
│ │ ├── pnpm dev:h5(Vite dev) │ │
│ │ └── 浏览器 HMR 实时更新 │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ │ API 代理 │
│ │ localhost:48080 │
│ │ │
│ ┌──────────────▼──────────────────┐ │
│ │ Docker 容器 │ │
│ │ ├── MySQL(3306) │ │
│ │ ├── Redis(6379) │ │
│ │ └── yudao-server(48080) │ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────┘
最佳实践清单
- Docker 只用于基础设施(MySQL、Redis、后端服务)
- 前端开发直接在 Windows 本地运行
- 项目根目录创建
.nvmrc统一 Node 版本 - Windows 使用
nvm use (Get-Content .nvmrc)或改用 fnm - 生产构建仍然用 Docker 多阶段构建,确保环境一致性
- 如需在 Docker 中跑前端,考虑使用 WSL2 原生文件系统(将项目放在 WSL2 的 Linux 文件系统内,而非 Windows 文件系统挂载进去)
关于 WSL2 原生文件系统的补充说明
如果一定要在 Docker 中跑前端,一种折中方案是:把项目放在 WSL2 的 Linux 文件系统内(如 /home/user/project/),而不是 Windows 文件系统(如 C:\Users\...)。这样 Docker 容器可以直接访问 WSL2 的原生 ext4 文件系统,inotify 事件可以正常工作。
但这种方式需要:
- 在 WSL2 中安装 Node.js 和开发工具
- 在 WSL2 中启动 Vite dev server
- 通过
\\wsl.localhost\在 Windows 浏览器中访问
对于不熟悉 Linux 命令行的开发者来说,学习成本较高。因此,对于大多数 Windows 开发者,推荐的方案仍然是前端本地跑 + Docker 管基础设施。
全文完。有疑问随时问。