第十章: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 快捷方式,指向另一个路径
卷 / VolumeDocker 里的持久化存储机制
挂载 / Mount把宿主机或卷接入容器的某个路径
BuildKitDocker 新的构建引擎,支持缓存等高级功能
monorepo单一仓库多个子项目(mono = 单一,repo = 仓库的简写)
CASContent-Addressable Storage,内容寻址存储(pnpm store 的实现机制)

10.7 一句话总结

pnpm 通过”硬链接”实现项目间共用一份依赖;Docker 通过”独立文件系统”实现容器与宿主机隔离。 要让两者结合,靠的是”挂载(mount)“——它在隔离的墙上开一扇门,让指定区域能共享。


最后更新:2026-06-02(据 git 提交历史回填,非内容重新审校日期)