第十章:pnpm 共用机制 与 Docker 隔离的碰撞
⚠️ 本章是进阶内容:如果你还没用过 Docker,或者还没碰到过”项目装依赖太慢/磁盘空间不够”的真实问题,建议直接跳过本章——等做过 1~2 个 Docker 项目后再回来看,理解会深刻得多。
这是一个”两个概念碰撞”的好问题: pnpm 强调”全局共用一份依赖”,但 Docker 又强调”每个容器完全隔离”。 那么 Docker 里用 pnpm 还有意义吗?怎么共用?
📎 本章不回答”npm 和 pnpm 我该选哪个”——那个问题在入门篇 01_工程全景与核心名词.md §「npm 和 pnpm 到底该用哪个」 (含命令对照表、幽灵依赖、按 lock 文件选择的原则)。本章假设你已经决定用 pnpm, 只讨论它和 Docker 一起用时的机制冲突。
10.1 pnpm 说的”共用”具体是怎么实现的?
技术名词:硬链接(hard link)+ 软链接(symbolic link / symlink)
先理解硬链接是什么
硬链接 ≠ 复制文件,而是给同一份文件起多个名字。
举例:你有一份照片 photo.jpg,文件实际数据在磁盘某个位置(假设占 5MB)。
ln /Users/<your-username>/photo.jpg /Users/<your-username>/Desktop/相册.jpg执行后:
- 磁盘上还是只有一份 5MB 的数据
- 但有两个”路径名”都能访问它
- 删除其中一个,另一个还在
- 改任何一个,另一个跟着变(因为是同一份数据)
这和”复制一份”完全不同! 复制是磁盘上多了一份 5MB;硬链接是磁盘上只有一份,多了一个”名字”。
软链接(symlink)vs 硬链接
| 类型 | 本质 | 类比 |
|---|---|---|
| 硬链接 (hard link) | 同一文件的多个名字(指向同一磁盘块) | 同一个人的不同绰号 |
| 软链接 (symlink) | 一个”快捷方式”,指向另一个路径 | Windows 桌面快捷方式 |
差别:
- 删除原文件 → 硬链接还能用(数据还在),软链接失效(断链)
- 跨磁盘 → 硬链接不行,软链接可以
pnpm 的实际工作方式
全局存储区(只有这里有真实文件)
~/.pnpm-store/
├─ react@19.1.0/ ← 真正的 react 文件,5MB,全电脑只此一份
├─ vite@6.3.2/ ← 真正的 vite 文件
└─ ...其他几千个包
项目A/node_modules/react/ ──硬链接──→ 指向全局存储区的 react@19.1.0
项目B/node_modules/react/ ──硬链接──→ 指向全局存储区的 react@19.1.0
项目C/node_modules/react/ ──硬链接──→ 指向全局存储区的 react@19.1.0
结果: 你电脑上有 100 个项目都用 react,磁盘上 react 只占 1 份 5MB 的空间。
📌 类比:“小区共用洗衣房”
- npm/yarn = 每家自己买一台洗衣机(每个项目存一份完整依赖)
- pnpm = 小区有一个公共洗衣房,每户人家拿钥匙就能用(每个项目链接到全局存储)
重要前提
硬链接只能在同一个文件系统/磁盘分区上工作,跨磁盘就不行。 所以 pnpm 的全局存储必须和你的项目在同一块硬盘上。
10.2 Docker 是怎么”隔离”的?
Docker 容器有自己独立的文件系统,跟你电脑(宿主机)的文件系统是完全隔开的。
你电脑(宿主机)
├─ /Users/<your-username>/... ← 你能看到
├─ ~/.pnpm-store/ ← 你能看到
└─ Docker 容器
├─ /app/ ← 容器内看到自己的 /app
├─ /root/ ← 容器内自己的 /root
└─ /... ← 整个文件系统是独立的
↑
容器里的进程看不到宿主机的任何东西
关键点:
- 容器里跑的程序,看到的”根目录
/”是容器自己的根目录 - 容器默认看不到宿主机的
~/.pnpm-store/ - 容器跑完销毁后,里面的文件全没了
10.3 那 Docker 里用 pnpm 还有意义吗?
有意义,但”共用”的范围变了。 分三种场景看:
场景 1:单个容器内部,依然能”共用”
很多大型项目是 monorepo(一个仓库多个子项目),比如:
my-monorepo/
├─ apps/web/ ← 子项目1,用 react
├─ apps/admin/ ← 子项目2,也用 react
└─ packages/ui/ ← 子项目3,也用 react
容器内跑 pnpm install 时,容器内部有自己的 .pnpm-store,三个子项目共用同一份 react。
这就是”容器内的小区共用洗衣机”——隔离的是容器跟宿主机,不是容器内部的子项目之间。
场景 2:多次构建之间——用 BuildKit 缓存”共用”
Docker 构建镜像可能每天跑很多次(每次代码改动都构建一次)。如果每次都重新下载依赖,太慢。
新版 Docker 的 BuildKit 提供”缓存挂载”功能:
# Dockerfile 里写
RUN --mount=type=cache,target=/root/.pnpm-store pnpm install意思是:「每次构建时,把宿主机上一个特殊的缓存目录临时挂进容器的 .pnpm-store 位置」。
第一次构建:下载 react → 存进缓存目录
↓
第二次构建:发现缓存里有 react → 直接用,不下载
↓
第三次构建:依然能用上次的缓存
这是”工地复用上次的建材,但施工完就退场”。
场景 3:开发阶段——用 Volume “把宿主机的洗衣房搬进容器”
开发时你可能想让容器直接用你电脑上已有的 pnpm 缓存:
docker run -v ~/.pnpm-store:/root/.pnpm-store my-image-v 是”挂载”参数,意思是:「把宿主机的 ~/.pnpm-store 这个文件夹,直接接到容器内的 /root/.pnpm-store 位置」。
宿主机 容器
~/.pnpm-store/ ←─挂载─→ /root/.pnpm-store/
(同一份数据,两边都能访问、都能改)
容器一启动,立刻就能”看到”你电脑里已下好的依赖,无需重新下载。
📌 挂载(mount)打破隔离的本质:在隔离的墙上开了一扇门,让两边能共享指定区域。
10.4 全局总结图
┌─────────────────────────────────────────────────────────────────┐
│ 你的电脑(宿主机) │
│ │
│ ┌─────────────┐ │
│ │ ~/.pnpm- │ │
│ │ store/ │←─── 项目A、B、C 都硬链接到这里(同一磁盘) │
│ └─────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Docker 容器(隔离的世界) │ │
│ │ │ │
│ │ ┌────────────┐ │ │
│ │ │/.pnpm-store│ ← 容器内子项目共用(场景1) │ │
│ │ └────────────┘ │ │
│ │ ↑ │ │
│ │ └─ 可以通过以下方式与宿主机连通: │ │
│ │ • -v 挂载(场景3) │ │
│ │ • BuildKit cache mount(场景2) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10.5 实战建议速查
| 你在干啥 | 建议怎么用 |
|---|---|
| 普通本地开发,没用 Docker | 直接用 pnpm,全自动享受全局共用 |
| Docker 内单纯跑个项目 | pnpm 的优势相对变小,但仍比 npm 快 |
| Docker 镜像构建(CI/CD) | 用 BuildKit 缓存挂载(场景 2) |
| Docker 里做开发调试 | 用 -v 挂载本地 store(场景 3) |
| 部署到生产服务器 | 通常不需要”共用”——镜像里只装需要的依赖即可 |
10.6 相关概念名词速查
| 概念 | 含义 |
|---|---|
| 硬链接 (hard link) | 同一份文件的多个”名字”,必须同磁盘 |
| 软链接 (symlink) | 类似 Windows 快捷方式,指向另一个路径 |
| 卷 / Volume | Docker 里的持久化存储机制 |
| 挂载 / Mount | 把宿主机或卷接入容器的某个路径 |
| BuildKit | Docker 新的构建引擎,支持缓存等高级功能 |
| monorepo | 单一仓库多个子项目(mono = 单一,repo = 仓库的简写) |
| CAS | Content-Addressable Storage,内容寻址存储(pnpm store 的实现机制) |
10.7 一句话总结
pnpm 通过”硬链接”实现项目间共用一份依赖;Docker 通过”独立文件系统”实现容器与宿主机隔离。 要让两者结合,靠的是”挂载(mount)“——它在隔离的墙上开一扇门,让指定区域能共享。
最后更新:2026-06-02(据 git 提交历史回填,非内容重新审校日期)