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.yaml defining your packages
  • Docker named-volume support (default on Docker Engine 24+)

How-To Steps

  1. 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 --version prints 9.7.0.

  2. 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" }
    }
    
  3. Define the workspace in pnpm-workspace.yaml:

    packages:
      - "apps/*"
      - "packages/*"
    
  4. Install with a frozen lockfile in postCreateCommand for reproducibility:

    { "postCreateCommand": "pnpm install --frozen-lockfile" }
    

    Verify: pnpm install a second time reports “Already up to date” and resolves from the store offline:

    pnpm install --frozen-lockfile --offline && echo "store hit"
    
  5. Run a script in one workspace package without leaving the root:

    pnpm --filter @org/web run build
    

Common Pitfalls

SymptomRoot CauseRemediation
Every rebuild re-downloads packagesStore inside the ephemeral container layerMount the store as a named volume
ERR_PNPM_NO_LOCKFILE in CILockfile not committed or --frozen-lockfile against driftCommit pnpm-lock.yaml; run install before tests
Phantom dependency errorspnpm’s strict, non-hoisted layout exposes undeclared depsAdd the missing dependency to that package’s package.json
Wrong pnpm versionCorepack not enabledcorepack 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.