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

160 阅读23分钟

Docker 前端本地开发指南(实操版)

本文只讲一件事:怎么用 Docker 把前端开发环境跑起来,并且能正常热更新。

如果你是想系统学习 Docker(生产部署、镜像仓库、K8s 等),请看完整版《Docker 开发与部署指南》;本文是它的"本地开发子集"——把日常开发真正用得上的部分提炼出来,砍掉所有延伸内容,照着做就行。

第八章是一次完整的踩坑实录(全量同步 → 首次启动路由失效 → 预生成修复),遇到怪问题可以直接跳过去看。

适用范围:yudao-ui-admin-vue3(Vite 8)/ yudao-ui-admin-uniapp(Vite 5)· Node 20 · pnpm 10 · Docker Desktop(WSL2 后端)

日期变更
初版基础方案:COPY 进镜像 + compose watch 热更新
2026-08-20新增第八章:启动前全量同步(--build)+ pages.json 路由预生成(gen-pages);新增 Q7;uniapp 容器 CMD 改为 dev:h5:docker

目录


一、💡 为什么要用 Docker 跑前端开发

痛点Docker 解决方案
团队成员 Node 版本不一致镜像锁定版本,人人一样
Windows 装某些 npm 包失败(如 node-sass)容器内是 Linux,原生兼容
新人入职装环境要半天docker compose up 一条命令搞定
切换项目时依赖冲突每个项目独立容器,互不干扰

一句话:把 Node 版本、依赖、启动命令全部固化进镜像,任何人、任何机器,一条命令拉出完全一致的开发环境。

一条命令,人人一样的开发环境转存失败,建议直接上传图片文件


二、🧩 核心概念速览(够用就行)

本地开发只需要理解三个词:

概念通俗理解本地开发里对应什么
镜像(Image)应用的"安装包",只读模板node:20-alpine + 你的项目依赖
容器(Container)镜像跑起来的"活实例"正在跑的 vite dev server
数据卷(Volume)Docker 管理的持久化存储node_modules、数据库数据

三者关系:

Dockerfile ──build──> Image(安装包)
                          │
                          │ docker compose up
                          ▼
                     Container(活实例)
                          │
                          │ 挂载
                          ▼
                     Volume(持久化数据)

本地开发只需要记住:容器 = 你项目的"独立运行环境",删了可以随时重建,不影响你的源码和依赖。


三、🛠️ 环境准备

3.1 安装 Docker Desktop

Windows 上装 Docker Desktop,安装后启动即可(默认用 WSL2 后端)。

3.2 配国内镜像加速(强烈建议)

国内直接访问 Docker Hub 经常超时,先配加速器:

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"
  ]
}

Apply & Restart 生效。

3.3 确认后端地址

前端开发时请求要转发到后端(yudao-server)。两种常见情况:

后端在哪前端配置
宿主机 IDE 里跑http://host.docker.internal:48080
局域网某台机器跑直接写 IP,如 http://192.168.1.182:48080

这个地址通过 compose 的 environment 注入(见下文 4.2),不用改代码。


四、📄 三个核心文件

每个前端项目目录下都要有这三个文件。以 yudao-ui-admin-uniapp 为例(vue3 项目的差异见第十二章)。

4.1 Dockerfile.dev

# 仅用于本地开发:跑 pnpm dev:h5,配合 compose watch 的 sync+restart 实现热更新
FROM node:20-alpine

# git 被 husky/precommit 间接需要;corepack 启用 pnpm@10
RUN apk add --no-cache git && corepack enable

WORKDIR /app

# 只拷贝依赖描述文件,利用 docker 层缓存。
COPY package.json pnpm-lock.yaml .npmrc ./
# --ignore-scripts: 跳过 prepare 钩子(init-husky / init-baseFiles),
# 因为 scripts/ 目录此时还没拷进来。pages.json 等文件由启动时的 gen-pages 预生成(见第八章)。
RUN pnpm install --frozen-lockfile --ignore-scripts

# 源码打进镜像(compose watch 的 sync 方案需要,bind mount 在 Windows 下 inotify 不穿透)
# 注意:dev:docker 的 --build 每次启动都会按"当前宿主机源码"重新执行此层(内容校验和缓存),
# 宿主机删除的文件不会进镜像 —— 这就是"启动前同步代码"的实现,无需任何脚本(见 8.1)
COPY . .

# dev server 端口(vite.config.ts 中 VITE_APP_PORT=9000)
EXPOSE 9000

# 默认 H5 开发模式
# 必须先跑 gen-pages 预生成 src/pages.json 再启动 uni(原因见 8.3):
# uni:h5 在 Vite config 钩子里读 pages.json(once 缓存,早于 UniPages 的 configResolved 生成时机),
# 容器冷启动没有该文件时整个会话路由都会失效(所有跳转被 fallback 到首页)
CMD ["pnpm", "run", "dev:h5:docker"]

三个关键点:

  • pnpm install 只拷了依赖描述文件 → 依赖装进镜像层,改代码不会重装;
  • COPY . . 把源码拷进镜像 → 不能删,热更新方案靠它(见第七章),同时它还承担"启动前全量同步"(见 8.1);
  • CMD 不是直接 dev:h5,而是 dev:h5:docker(先预生成 pages.json 再启动,见 8.4)。

4.2 docker-compose.dev.yml

命名约定(本项目):镜像名 / 容器名 / project 名统一为纯项目名,不带 -dev / -vue3-dev 后缀。即 vue3 项目为 yudao-ui-admin-vue3、uniapp 项目为 yudao-ui-admin-uniapp。这是为避免 Docker Desktop 列表里出现多余后缀、保持命名一致而定的团队约定,两个项目务必延续。

# 镜像名 / 容器名 / project 名 统一为纯项目名(不带 -dev / -uniapp-dev 后缀)
# 启动: pnpm dev:docker  (= up --watch --build:先按当前源码重建镜像再启动,宿主机删的文件不会带进容器)
# 必须用 `up --watch` 启动; Docker Desktop 直接点 Start 没有 watch 会话、不会同步文件
name: yudao-ui-admin-uniapp

services:
  yudao-ui-admin-uniapp:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: yudao-ui-admin-uniapp:latest
    container_name: yudao-ui-admin-uniapp
    ports:
      - '9000:9000'
    develop:
      watch:
        # 源码改动:仅同步进容器,Vite 原生 HMR(保留页面状态)
        - action: sync
          path: ./src
          target: /app/src
          # pages.json 由容器内 gen-pages 生成,防止宿主机的旧文件被推送覆盖(见 8.4)
          ignore:
            - pages.json
        - action: sync
          path: ./index.html
          target: /app/index.html
        # 根目录配置文件改动:需重启 vite(配置无法热加载)
        - action: sync+restart
          path: ./pages.config.ts
          target: /app/pages.config.ts
        - action: sync+restart
          path: ./manifest.config.ts
          target: /app/manifest.config.ts
        - action: sync+restart
          path: ./uno.config.ts
          target: /app/uno.config.ts
        - action: sync+restart
          path: ./vite.config.ts
          target: /app/vite.config.ts
        # 环境变量改动:需重启 vite
        - action: sync+restart
          path: ./env
          target: /app/env
        # 依赖改动:重建镜像
        - action: rebuild
          path: ./package.json
        - action: rebuild
          path: ./pnpm-lock.yaml
    environment:
      # 后端 yudao-server 地址(见 3.3)
      - VITE_SERVER_TARGET=http://host.docker.internal:48080
      # 容器里没有微信开发者工具,跳过 openDevTools 插件
      - SKIP_OPEN_DEVTOOLS=true
    extra_hosts:
      # Linux 上等同 host.docker.internal 指向宿主机;Windows/Mac 自带
      - 'host.docker.internal:host-gateway'
    # 保持 stdin 开放 + tty,便于 Ctrl+C 优雅退出 vite
    stdin_open: true
    tty: true

develop.watch 三种动作的含义:

动作触发时机效果
sync源码文件(src/)变化只把文件同步进容器,Vite 原生 HMR,不重启
sync+restart配置文件(vite.config.ts 等)变化同步后重启 vite(配置必须重启才生效)
rebuildpackage.json / pnpm-lock.yaml 变化重新构建镜像(依赖变了,要重装)

4.3 .dockerignore

node_modules
dist
.git
docs
.image
.husky
.vscode
.github
*.log
*.local
tmp/
.playwright-cli/
private.key
private.*.key
.idea
.hbuilderx
.stylelintcache
.eslintcache
docs/.vitepress/dist
docs/.vitepress/cache
# 生成类文件:交给容器内 gen-pages 按需生成,不带旧缓存进镜像(见 8.3/8.4)
src/types
src/pages.json
src/pages-json-js
# 裁剪掉的业务分包目录:不进镜像,也避免本地残留误打包
src/pages-ai
src/pages-erp
src/pages-im
src/pages-infra
src/pages-iot
src/pages-mall
src/pages-member
src/pages-mes
src/pages-mp
src/pages-pay
src/pages-statistics
src/pages-system
src/pages-wms
*.ps1

排除无关文件,缩小构建上下文、加快构建。其中 src/pages.json 是故意排除的:它是 dev 启动时的生成物(不是源码),带旧文件进镜像反而会引发路由问题(见第八章)。


五、🚀 第一次启动(初始化)

cd yudao-ui-admin-uniapp
docker compose -f docker-compose.dev.yml up --watch --build

或(项目里已封装好脚本):

pnpm dev:docker

首次启动会做四件事:拉基础镜像 → 构建项目镜像(装依赖)→ 按当前宿主机源码执行 COPY . .(启动前全量同步,见 8.1)→ 启动容器并进入 watch 模式。首次构建需要几分钟,之后启动:源码没变则构建层全部缓存命中(秒级),有变则只重建 COPY 那一层(1~2 秒)。

启动成功后,日志依次出现:

[gen-pages] 已生成 src/pages.json(主包 9 页 + 分包 86 页)   ← 路由预生成(见 8.4)
ready in xxxx ms                                              ← vite 就绪

浏览器访问:

http://localhost:9000/admin-ui-uniapp/

注意:命令里的 --watch 不能省,这是热更新的关键(见第七章);uniapp 项目的 dev:docker 已内置 --build,实现"启动即同步最新代码"(见 8.1)。


六、☕ 日常开发流程

6.1 每天开工

cd yudao-ui-admin-uniapp
pnpm dev:docker

6.2 改代码 → 自动热更新

在 IDE 里正常改 src/ 下的代码,保存即生效:

  • 改组件/页面 → Vite 原生 HMR,毫秒级,保留页面状态(不刷新页面);
  • 改 vite.config.ts 等配置文件 → 自动重启 vite(约 2~5 秒);
  • 改 package.json 依赖 → 自动重建镜像(需要网络)。

6.3 收工

# 停止容器(保留镜像和数据,下次秒启)
docker compose -f docker-compose.dev.yml stop
# 或直接 Ctrl+C(up --watch 在前台跑时)

6.4 什么时候需要重建

场景命令
改代码(日常)什么都不用做,自动热更新
隔天开工 / 上次改过文件后重新启动pnpm dev:docker(uniapp 版已带 --build,启动即全量同步,见 8.1)
改 Dockerfile.dev 或依赖(package.json)pnpm dev:docker:rebuild(--build --no-cache,完全重建)
改 compose 文件重新 pnpm dev:docker

七、🔥 热更新:正确姿势与原理(重点)

7.1 症状

Windows + Docker Desktop 下,如果按网上教程用 bind mount(volumes: - .:/app)方式挂源码,改代码后浏览器不刷新,必须重启容器才生效。这是 Windows 上 Docker 跑前端最常见的坑。

7.2 根因

Docker Desktop 在 Windows 上通过 WSL2/Hyper-V 的虚拟文件共享层把宿主机目录挂进容器。这个共享层不会把宿主机侧的文件变更事件转发进容器——容器内的 Vite/chokidar 收不到"文件变了"的通知,HMR 自然不触发。

bind mount 方案:
宿主机改代码 → 文件在宿主机侧变化
                    │
                    ▼
          虚拟文件共享层 ⚠️ 变更事件在这里丢失
                    ▼
        容器内 Vite 收不到通知 → HMR 不触发

7.3 正确方案:COPY + compose watch 的 sync

思路:不靠 bind mount,改为「源码 COPY 进镜像 + compose watch 在宿主机侧监听文件、把改动写进容器」。文件是在容器侧写入的(本地写入),Vite 的原生文件监听能正常收到事件,原生 HMR 即可工作。

正确方案:
宿主机改代码
     │  compose watch 在宿主机侧监听(Windows 原生监听,正常)
     ▼
sync 把新文件写进容器(容器侧写入,事件正常)
     ▼
Vite 原生 HMR 检测到变化 → 模块热替换(保留状态,毫秒级)

上:bind mount 事件丢失;下:COPY + watch sync 畅通转存失败,建议直接上传图片文件

这就是第四章那份配置的来源:Dockerfile.dev 里 COPY . . 打源码,compose 里 src 用 sync。

7.4 为什么必须 up --watch,点 Start 不行

Docker Desktop 的 Start 按钮只是 docker start(拉起容器进程),不会启动 compose watch 的宿主机侧监听会话。没有 watch 会话就没有 sync,容器里跑的是镜像里的源码快照——你改的代码根本进不了容器,自然不热更新。

启动方式watch 会话文件同步热更新
docker compose up --watch✅ 有✅ sync✅
Docker Desktop 点 Start❌ 没有❌ 无同步❌ 跑的是镜像快照
docker compose up -d(普通后台)❌ 没有❌ 无同步❌ 跑的是镜像快照

7.5 为什么不需要 node_modules 命名卷

旧方案(bind mount)必须挂一个 node_modules 命名卷,因为 .:/app 会把宿主机 Windows 的 node_modules 一起挂进来——里面的 .exe 二进制在 Linux 容器里跑不了,得用命名卷把容器里的 Linux 版"盖回来"。

新方案源码是 COPY 进镜像、没有 bind mount,宿主机不会污染容器内的 node_modules,sync 也只同步 ./src,所以命名卷不需要了(还能省几百 MB 磁盘)。

7.6 终极推荐:前端本地跑 + Docker 管基础设施

如果团队不强制"前端也必须在 Docker 里",最佳体验是前端在 Windows 本地原生跑(HMR 毫秒级、无中间层),Docker 只负责 MySQL、Redis、后端:

docker compose up -d mysql redis    # 启动基础设施
cd yudao-ui-admin-uniapp
nvm use 20                          # 切 Node 版本
pnpm install
pnpm dev:h5                         # 前端本地跑,改代码即时 HMR
方案HMR 延迟页面状态复杂度
前端本地跑 + Docker 管基础设施<100ms✅ 保留低(推荐)
Docker 跑前端(COPY + sync)毫秒级✅ 保留中(本文方案)
Docker 跑前端(bind mount)❌ 不生效--

如果你选择了全容器化路线(团队统一环境),第八章的「启动前全量同步 + 路由预生成」是这套方案的必要补丁,别漏。

7.7 Docker Desktop 容器列表折叠 与 docker run 方案权衡

7.7.1 折叠是 Docker Desktop 的 UI 行为,关不掉

用 docker compose up --watch 起的容器,在 Docker Desktop 的 Containers 列表里永远会按 project name 分组显示成 组名 ▸ 子容器 的两层折叠结构(即使 project 名 = 容器名,也仍有一层 group 框在外)。

这不是配置问题,是 Docker Desktop 自身的硬行为——任何 compose 管理的容器都按 project 分组,没有设置项能关掉折叠。

7.7.2 为了「一行平铺」试过 docker run,但牺牲了 HMR 速度

实测过用 docker run 直接起容器(不走 compose、无 project 分组,列表真一行平铺),源码用 bind mount 挂载 + Vite server.watch.usePolling 轮询文件变化:

docker run -d --name yudao-ui-admin-uniapp -p 9000:9000 \
  --add-host host.docker.internal:host-gateway \
  -e VITE_DOCKER=true \
  -v "$(pwd)/src:/app/src" -v "$(pwd)/vite.config.ts:/app/vite.config.ts" \
  yudao-ui-admin-uniapp:latest
// vite.config.ts:Windows bind mount 不转发 inotify,开 polling 绕开
server: {
  watch: process.env.VITE_DOCKER === 'true'
    ? { usePolling: true, interval: 300,
        ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/types/**'] }
    : undefined,
}

结果:HMR 能工作(polling 确实检测到了文件变化),但很慢——一个 55KB 的 .vue 文件改动后 HMR 需要 5~6 秒才刷新。瓶颈在 Windows Docker Desktop 的 9P 文件共享层:每次读文件都要跨进程(宿主→Docker→容器),I/O 开销是平台级的,调 interval 也压不下去。

7.7.3 结论:保留 compose watch,接受折叠
启动方式Docker Desktop 列表HMR 延迟取舍
docker compose up --watch折叠(project group)<1 秒折叠可接受,HMR 最快
docker run + bind mount + polling一行平铺5~6 秒(55KB 文件)为去折叠牺牲速度,不划算

本项目最终决定:用 docker compose up --watch(纯项目名 + compose watch),HMR 毫秒级、保留页面状态;Docker Desktop 的折叠 UI 是已知限制,接受它。不要再为了「一行平铺」改回 docker run + polling。


八、🧯 启动前全量同步与 pages.json 路由预生成(uniapp 排查实录)

本章记录两个真实问题的完整解决历程:先解决"启动前全量同步代码",同步解决后才暴露"首次启动路由失效",两者环环相扣。vue3 项目没有 pages.json 动态生成机制,不受影响。

8.1 问题一:容器残留旧文件,启动前全量同步

症状:宿主机删掉的文件(如裁剪掉的页面)在容器里残留,导致容器内构建报错;容器里的 src 是旧代码。

走过的弯路:先尝试了 entrypoint.dev.sh + rsync --delete 方案(宿主机 src 只读挂载进容器,启动时 rsync 全量对账)。能用,但引入了额外脚本、启动变慢、且与 compose watch 的 sync 机制职责重叠,后废弃。

最终方案(零脚本):dev:docker 加 --build,利用 Docker 构建缓存实现"启动前全量同步":

"dev:docker": "docker compose -f docker-compose.dev.yml up --watch --build"

原理:--build 让每次启动都先走镜像构建,其中 COPY . . 层按**内容校验和(content checksum)**缓存——

  • 宿主机文件没变 → 该层缓存命中,不重新拷贝(启动秒级);
  • 宿主机文件有变(新增/修改/删除)→ 该层按"当前宿主机源码"重建,宿主机删除的文件自然不会进镜像。
pnpm dev:docker = up --watch --build
        │
        ▼
镜像构建:COPY . . 层(内容校验和缓存)← 这一步就是"启动前全量同步"
        │
        ▼
镜像有更新 → 容器 Recreate(全新可写层)
镜像无更新 → 容器仅 restart(可写层保留)

这就是 4.1 节 Dockerfile 里那条注释的含义:不需要任何同步脚本,构建缓存本身就是同步机制。

8.2 问题二:首次启动路由失效,第二次才正常

全量同步修好后,暴露出新问题:文件有变化时首次启动,所有路由跳转失效——无论点哪里都被 fallback 到首页,只有第二次执行 dev:docker 才恢复正常。

这个现象正是 8.1 方案的"副作用"链条:

文件有变 → COPY 层重建 → 镜像更新 → 容器 Recreate(不是 restart!)
        → 可写层整体丢弃 → 上次运行生成的 src/pages.json 一并消失 → 冷启动

而"第二次正常"的原因:源码没变 → 构建层缓存全命中 → 镜像没更新 → 容器只 restart 不 recreate → 可写层保留 → 上次运行时插件已写好的 pages.json 还在。

排查方法:docker exec -it yudao-ui-admin-uniapp sh 进容器,对比冷启动后 vite 首次启动与第二次启动时 src/pages.json 的内容差异;再读 uni-app 和 uni-pages 两个插件的源码,定位到下述时序冲突。

8.3 根因:pages.json 的读取时机早于生成时机

两个插件在同一次 Vite 启动中抢同一个文件 src/pages.json:

角色插件Vite 钩子行为
读取方uni 官方 uni:h5config(早)读 pages.json,且 parsePagesJsonOnce = once(...) 结果永久缓存
生成方@uni-helper/vite-plugin-uni-pagesconfigResolved(晚)扫描页面文件,写入真实路由

Vite 保证所有 config 钩子先于所有 configResolved 执行。冷启动时(容器里没有 pages.json,.dockerignore 也故意排除了它):

  1. UniPages 插件工厂发现文件不存在,先写占位文件 {pages:[{path:""}]}
  2. uni:h5 在 config 钩子读到空占位 → once() 缓存固化
  3. UniPages 到 configResolved 才写入 95 条真实路由——但缓存已固化,整个 dev 会话都用空路由
  4. 表现为:所有跳转 fallback 首页、返回无效

读取方先到却拿到空文件,生成方迟到一步转存失败,建议直接上传图片文件

本地 Windows 直接 pnpm dev:h5 不触发这个问题,是因为本地 src/pages.json 一直存在(上次运行留下的),uni:h5 读到的永远是完整文件——只有"全新环境首次启动"(新容器、新 clone、CI)才会踩中。

8.4 修复:启动前预生成 pages.json

思路:让 pages.json 在 uni 启动之前就已就位,uni:h5 的 config 钩子第一次读到的就是完整路由。共改 4 个文件:

文件改动
scripts/gen-pages.mjs(新增)用 vite.resolveConfig 只走一遍插件生命周期(不启服务、不编译),预写 src/pages.json / src/manifest.json,并校验路由非空才放行
package.json新增 "dev:h5:docker": "node ./scripts/gen-pages.mjs && uni"
Dockerfile.devCMD 改为 pnpm run dev:h5:docker(先预生成再启动)
docker-compose.dev.ymlwatch 的 src sync 加 ignore: pages.json,防止宿主机残留旧文件被推送覆盖容器内生成的

gen-pages.mjs 的核心逻辑(完整文件见项目 scripts/ 目录):

// 关键点 1:UNI_PLATFORM 必须在动态 import 插件之前设置
//(@uni-helper/uni-env 在模块加载时求值,ESM 静态 import 会被提升导致拿不到值)
process.env.UNI_PLATFORM ||= 'h5'
const { resolveConfig } = await import('vite')

// 关键点 2:resolveConfig 只走 config / configResolved 等钩子,
// 不启 dev server、不编译 —— 秒级完成,让 UniPages 把 pages.json 写好。
// 注意:不加载项目 vite.config.ts(configFile: false),只挂生成配置文件所需的插件,
// UniPages 的 exclude / subPackages 参数从 vite.config.ts 原样复制
await resolveConfig(
  { configFile: false, root, logLevel: 'error', plugins: [UniPlatform(), UniPages({...}), UniManifest()] },
  'serve',
)

// 关键点 3:pages.json 带注释(插件用 comment-json 写入),校验时也要用 comment-json 解析;
// 路由为空直接 exit(1) 中止启动,避免带着坏配置起 dev
const { parse } = await import('comment-json')
const pagesJson = parse(fs.readFileSync(pagesPath, 'utf-8'))
if (!pagesJson.pages?.length || !pagesJson.pages[0].path) {
  console.error('[gen-pages] src/pages.json 生成异常:主包路由为空')
  process.exit(1)
}

8.5 验证与维护注意

验证(刻意制造"文件有变化后首次启动"场景):

[gen-pages] 已生成 src/pages.json(主包 9 页 + 分包 86 页)   ← 先生成
command, mode ->  serve development                            ← 后启动
ready in 3207ms · HTTP 200

容器 Recreate(冷启动)后首次启动路由即完整(9 + 86 = 95 条),不再需要第二次执行。

维护注意(重要):

  • gen-pages.mjs 里 UniPages 的参数(exclude / subPackages 分包目录)是从 vite.config.ts 复制的。以后调整分包目录时,两处要同步改(脚本内有注释提醒,忘了改会导致预生成路由与实际构建路由不一致);
  • 三个环节缺一不可:.dockerignore 排除 pages.json(不带旧文件进镜像)→ CMD 先 gen-pages(冷启动前生成)→ watch ignore pages.json(防宿主机旧文件覆盖)。

九、🩺 常见问题排查

Q1:pnpm install 时报 MODULE_NOT_FOUND: create-base-files.js

原因:pnpm install 自动跑 prepare 脚本,但构建时 scripts/ 目录还没拷进镜像。

解决:加 --ignore-scripts 跳过 prepare 钩子(Dockerfile.dev 里已加)。pages.json / manifest.json 等生成文件由容器启动时的 gen-pages 预生成(见 8.4)。

Q2:构建时报 Corepack 下载 pnpm 失败 / 拉镜像超时

原因:国内网络访问 registry.npmjs.org / Docker Hub 不稳定。

解决:配镜像加速(见 3.2)。注意:改过 package.json 或 Dockerfile.dev 会触发重新构建,此时需要网络,确保加速器生效后再构建。

Q3:改代码不热更新(Windows + Docker)

先按优先级排查:

  1. 是不是用 up --watch 启动的? 点 Start 或 up -d 都不会热更新(见 7.4)。
  2. compose 里 src 是不是 sync? 如果配成了 sync+restart,会变成全量重启(丢状态、慢 2~5s),改成 sync 就是原生 HMR。
  3. 改的是不是 src/ 下的文件? 配置文件本来就要重启,不算 bug。
  4. 改的内容是不是"有效内容"? 测试热更新时,往 .vue 文件 </style> 之后 append 注释是无效的——那是 SFC 语法外区域,vue 编译器会忽略,编译产物没变化,Vite 自然不会触发 HMR。要验证请在 <script> 里加一行真实代码。

Q4:容器里访问不到宿主机后端

解决:确认 compose 里有 extra_hosts: - "host.docker.internal:host-gateway",且 VITE_SERVER_TARGET 指向 http://host.docker.internal:48080(后端在宿主机跑时)。

Q5:HMR 有反应,但页面刷成空白 / 状态丢了

原因:改的文件无法热替换(比如改了 main.ts、路由配置、全局样式),Vite 自动降级为整页刷新,这是正常行为,不是故障。

Q6:Windows 上 nvm use 报 A version argument is required

Windows 用的是 nvm-windows,它不读 .nvmrc,必须手动传版本号:

nvm use 20
# 或从 .nvmrc 读:
nvm use (Get-Content .nvmrc)

Q7:uniapp 容器首次启动后所有路由跳转都进首页(第二次启动才正常)

这是 pages.json 生成时机与 uni 读取时机的冲突,已通过 gen-pages 预生成修复(见第八章完整分析)。如果再次出现,按顺序检查三个环节是否齐全:

  1. Dockerfile.dev 的 CMD 是不是 pnpm run dev:h5:docker(而非直接 dev:h5);
  2. .dockerignore 是否排除了 src/pages.json;
  3. compose 的 src sync 是否带 ignore: pages.json。

进容器看启动日志,第一行应出现 [gen-pages] 已生成 src/pages.json。


十、🖥️ Docker Desktop 图形界面日常操作

10.1 查看日志

打开 Docker Desktop → Containers → 点容器 → Logs 标签页。与命令行 docker logs -f 完全等价,显示的是同一份输出。

操作方法
实时跟踪点右上角 ▶️ Follow log
筛选关键字顶部搜索框输入
清屏(不影响后台)🗑️ Clear

10.2 启停容器

注意:Start/Stop 只适合"临时暂停恢复",不是日常热更新入口。 要让代码同步进容器,必须用 up --watch 启动(见 7.4)。

操作说明
容器旁 ⏸️ Stop等价 docker stop,暂停进程,数据保留
容器旁 ▶️ Start等价 docker start,不带 watch,仅恢复进程
彻底删除docker compose down(保留镜像和数据卷)

10.3 看磁盘占用

docker system df

清理无用资源:

docker container prune    # 删停止的容器
docker image prune -a     # 删未使用的镜像
docker volume prune       # 删未引用的数据卷

十一、⌨️ 快捷命令速查

项目 package.json 里封装了常用命令(以 uniapp 项目为例):

命令作用等价命令
pnpm dev:docker启动开发容器(带 watch + build,启动即全量同步)docker compose -f docker-compose.dev.yml up --watch --build
pnpm dev:docker:stop停止并删除容器docker compose -f docker-compose.dev.yml down
pnpm dev:docker:rebuild无缓存重建镜像并启动docker compose -f docker-compose.dev.yml up --watch --build --no-cache
pnpm dev:docker:logs查看日志docker compose -f docker-compose.dev.yml logs -f
pnpm dev:h5:docker容器内实际执行:预生成 pages.json 后启动 uninode ./scripts/gen-pages.mjs && uni

十二、⚖️ 两个项目的配置对照(vue3 / uniapp)

两个项目都已按本文方案配好,差异只在端口、环境变量、watch 条目和路由预生成机制:

项vue3(yudao-ui-admin-vue3)uniapp(yudao-ui-admin-uniapp)
镜像名 / 容器名 / project 名yudao-ui-admin-vue3yudao-ui-admin-uniapp
Docker Desktop 列表折叠(project group,无法关闭,见 7.7)同左
端口8081:809000:9000
Vite 版本8.0.105.2.8
后端代理变量VITE_PROXY_TARGETVITE_SERVER_TARGET
watch 的 srcsyncsync(且 ignore: pages.json,见 8.4)
watch 的配置文件vite.config.ts / build / .envvite.config.ts / pages.config.ts / manifest.config.ts / uno.config.ts / env
容器 CMDpnpm dev(直接起 vite)pnpm run dev:h5:docker(先 gen-pages 再起 uni,见 8.4)
dev:docker 是否带 --build否(up --watch)是(up --watch --build,启动即全量同步,见 8.1)
启动命令pnpm dev:dockerpnpm dev:docker
访问地址http://localhost:8081/http://localhost:9000/admin-ui-uniapp/

两个项目结论一致:Vite 5 和 Vite 8 的 chokidar 都正常,COPY + sync 方案下原生 HMR 都能工作。第八章的"启动前全量同步 + pages.json 预生成"目前只在 uniapp 项目落地(vue3 没有动态生成 pages.json 的机制,不需要);若 vue3 项目也遇到"启动时残留旧文件"问题,把它的 dev:docker 加上 --build 即可复用同样方案。


附录:📎 常用命令速查表

# 启动(带 watch + build:uniapp 日常入口,启动即全量同步 + 路由预生成)
docker compose -f docker-compose.dev.yml up --watch --build

# 停止(保留容器/镜像/数据)
docker compose -f docker-compose.dev.yml stop

# 彻底删除容器(保留镜像和数据卷)
docker compose -f docker-compose.dev.yml down

# 无缓存重建镜像并启动(改了 Dockerfile 或依赖异常时)
docker compose -f docker-compose.dev.yml up --watch --build --no-cache

# 查看日志
docker compose -f docker-compose.dev.yml logs -f <服务名>

# 查看容器状态
docker ps

# 查看磁盘占用
docker system df

# 进入容器调试(看 pages.json 生成结果、排查路由问题)
docker exec -it yudao-ui-admin-uniapp sh
cat /app/src/pages.json | head -20

最后提醒一句:本地开发最顺手的还是"前端本地跑 + Docker 管基础设施";如果团队要求前端也容器化,用本文第四、七章的 COPY + sync 方案即可,两条路都能拿到毫秒级 HMR 和保留页面状态。