Using pnpm workspaces in a DevContainer
pnpm’s content-addressable store and symlinked node_modules make monorepo installs fast and strict — until a DevContainer rebuild wipes the store and every pnpm install re-downloads from scratch, or a bind-mounted node_modules collides with the host’s. This page sets up a pnpm workspace so the store persists across rebuilds and the symlink layout stays intact. It extends the Node.js and TypeScript Workspace Configuration cluster.
Prerequisites
- Node.js 20+ with Corepack enabled
- A
pnpm-workspace.yamldefining your packages - Docker named-volume support (default on Docker Engine 24+)
How-To Steps
-
Enable pnpm through Corepack so the version is pinned by
packageManager:{ "image": "mcr.microsoft.com/devcontainers/typescript-node:20@sha256:7c3e...", "onCreateCommand": "corepack enable && corepack prepare pnpm@9.7.0 --activate", "remoteUser": "vscode" }Verify:
pnpm --versionprints9.7.0. -
Persist the pnpm store in a named volume so rebuilds reuse downloads:
{ "mounts": [ "source=pnpm-store,target=/home/vscode/.local/share/pnpm/store,type=volume" ], "containerEnv": { "PNPM_HOME": "/home/vscode/.local/share/pnpm" } } -
Define the workspace in
pnpm-workspace.yaml:packages: - "apps/*" - "packages/*" -
Install with a frozen lockfile in
postCreateCommandfor reproducibility:{ "postCreateCommand": "pnpm install --frozen-lockfile" }Verify:
pnpm installa second time reports “Already up to date” and resolves from the store offline:pnpm install --frozen-lockfile --offline && echo "store hit" -
Run a script in one workspace package without leaving the root:
pnpm --filter @org/web run build
Common Pitfalls
| Symptom | Root Cause | Remediation |
|---|---|---|
| Every rebuild re-downloads packages | Store inside the ephemeral container layer | Mount the store as a named volume |
ERR_PNPM_NO_LOCKFILE in CI | Lockfile not committed or --frozen-lockfile against drift | Commit pnpm-lock.yaml; run install before tests |
| Phantom dependency errors | pnpm’s strict, non-hoisted layout exposes undeclared deps | Add the missing dependency to that package’s package.json |
| Wrong pnpm version | Corepack not enabled | corepack enable in onCreateCommand; set packageManager |
Conclusion
The invariant is store persistence plus a frozen lockfile: pin pnpm via Corepack, keep the content-addressable store in a named volume, and install with --frozen-lockfile. Then a rebuild costs symlink creation, not a fresh download, and every developer resolves the identical dependency graph.
FAQ
Why does pnpm complain about packages that npm accepted? pnpm does not hoist by default, so undeclared (“phantom”) dependencies that npm silently exposed now fail. Declare them explicitly in the consuming package — this is correct, stricter behaviour.
Can I share the store with the host? Avoid bind-mounting the host store; OS and permission differences cause corruption. A named volume is container-local, persistent, and safe across rebuilds.
Related
- Node.js and TypeScript Workspace Configuration — the parent cluster.
- Configuring TypeScript path aliases in a DevContainer — aliases across workspace packages.
- Fixing Node.js npm cache in DevContainers — the npm equivalent of store persistence.