前端 Docker 开发与生产部署指南

55 阅读30分钟

前端 Docker 开发与生产部署指南

本文档以 yudao-ui-admin-uniapp 项目(Vue3 + uniapp + vite + pnpm)为例,系统讲解前端项目在 Docker 环境下的开发与生产部署。所有概念均从零讲起,无需 Docker 基础。

但是作为前端开发环境使用,还是不推荐使用 docker,我遇到的问题是开发过程中卡在了实时更新这个问题上,导致最终回到了 nvm 的方式。


目录


一、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-alpineyudao-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 builddocker 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 空间清理建议(按收益排序)

  1. 删 Unused 镜像:Images 页面 → 删 Unused 状态的镜像
  2. 删停止的容器:Containers 页面 → 删 Exited 状态的容器
  3. 删废弃的数据卷:Volumes 页面 → 删没被任何容器引用的卷
  4. 一键全清: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
nvmcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bashnvm use 时手动读
fnmcurl -fsSL https://fnm.vercel.app/install | bash配合 shell hook,cd 进目录自动切换
voltacurl 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.jsonengines.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 >= 20packageManager: pnpm@10.10.0node:20-alpine + corepack
平台二进制@esbuild/darwin-arm64@esbuild/darwin-x64@rollup/rollup-darwin-x64 写死不能把宿主机 node_modules 挂进容器,必须容器内 pnpm i
dev serverhost: '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.jsonscripts/ 目录还没进去。

解决:加 --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.tsserver 配置加 hmr.hosthmr.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_modulesDocker 管理,路径不透明依赖缓存、数据库数据
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 生产必备挂了自动重启
服务名互通而非 IPDocker 自动 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.confnginx 配置模板生产
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.devpnpm 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 → 左下角 TroubleshootClean / 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 数据迁到其他盘。

步骤

  1. 停止 Docker Desktop:右下角托盘右键 → Quit Docker Desktop

  2. 复制虚拟磁盘文件到新位置:

    源: C:\Users\<用户名>\AppData\Local\Docker\wsl\data\ext4.vhdx
    目标: D:\Docker\ext4.vhdx
    
  3. 修改 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.jsonscripts 里,与项目现有 pnpm devpnpm 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:dockerdocker compose -f docker-compose.dev.yml up
pnpm dev:docker:stopdocker compose -f docker-compose.dev.yml down
pnpm dev:docker:rebuilddocker compose -f docker-compose.dev.yml up --build
pnpm dev:docker:logsdocker 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 AppHBuilderX / 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 改代码保存后:

  1. Docker 容器内自动检测文件变化
  2. 自动重新编译
  3. 产物更新到 dist/dev/ 目录
  4. 开发者工具自动刷新(需在开发者工具中开启"自动编译")

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 serveruni watch 编译uni watch 编译
预览工具浏览器开发者工具(宿主机)HBuilderX(宿主机)
产物位置dist/dev/mp-xxxdist/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 命名卷持久化,避免重复下载 SDK
  • uniapp_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.ymlH5 开发无(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-server2 核1-2 GBJVM 吃内存,给足
mysql1-2 核1-2 GB数据库缓存需要内存
redis0.5 核512 MBRedis 本身很省
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 次
对比
策略容器挂了手动 stopDocker 重启适合场景
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 学习路径建议

按你的情况,建议按以下顺序学习:

立即掌握(生产必备)
  1. 资源限制(18.1)+ 日志轮转(18.2)→ 否则早晚出事
  2. 重启策略(18.3)→ unless-stopped
  3. 镜像仓库(18.4)→ 部署到服务器要用
  4. 多环境配置(18.5)→ dev/prod 切换
  5. 敏感信息管理(18.7)→ 不能把密码写代码里
用到再学
  1. 调试技巧(18.6)
  2. 健康检查自定义(18.10)
  3. Docker 网络(18.11)
进阶(一般用不上)
  1. 多架构构建(18.8)
  2. CI/CD 集成(18.9)
  3. 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-windowsgithub.com/coreybutler…),它与 Linux/Mac 上的 nvm-shgithub.com/nvm-sh/nvm)是两个完全不同的项目,API 和行为不一致:

维度nvm-sh(Linux/Mac)nvm-windows(Windows)
开发语言Shell 脚本Go 语言
安装方式curl 脚本Windows 安装程序
.nvmrc 自动读取nvm use 自动读取❌ 不支持
nvm use 命令不传参数时读 .nvmrc必须传版本号参数
配置文件Shell 脚本加载环境变量 NVM_HOMENVM_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:h5Vite dev)    │  │
│  │  └── 浏览器 HMR 实时更新        │  │
│  └──────────────┬──────────────────┘  │
│                 │                      │
│                 │  API 代理             │
│                 │  localhost:48080      │
│                 │                      │
│  ┌──────────────▼──────────────────┐  │
│  │  Docker 容器                    │  │
│  │  ├── MySQL3306)              │  │
│  │  ├── Redis6379)              │  │
│  │  └── yudao-server48080)      │  │
│  └─────────────────────────────────┘  │
└──────────────────────────────────────┘
最佳实践清单
  • 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 事件可以正常工作。

但这种方式需要:

  1. 在 WSL2 中安装 Node.js 和开发工具
  2. 在 WSL2 中启动 Vite dev server
  3. 通过 \\wsl.localhost\ 在 Windows 浏览器中访问

对于不熟悉 Linux 命令行的开发者来说,学习成本较高。因此,对于大多数 Windows 开发者,推荐的方案仍然是前端本地跑 + Docker 管基础设施


全文完。有疑问随时问。