Files
Zoltan Kochan 4e6351952d docs: correct the guides from agent skills to lockfile (#17248)
Correct the v12 guides from agent-skills.md through lockfile.md against
pnpm/crates, and fix the same wrong text on the v11 twins where pnpm11
confirms it. Part of pnpm/tasks#191.

Wrong claims (both versions):
- configuring: the global auth file is auth.ini, not rc; list the
  npm_config_* exceptions (workspace dir, userconfig, URL-scoped auth).
- config-dependencies: the hook import used an invalid ESM specifier.
- docker: Example 3 ran fetch --prod and built without an offline install
  or pnpm-workspace.yaml.
- errors: drop MISMATCHED_RELEASE_CHANNEL, describe INVALID_NODE_VERSION
  through nodeVersion, PEER_DEP_ISSUES needs strictPeerDependencies.
- faq: no lockfile deletion after a readPackage hook, default store order,
  junctions depend on symlink privilege.
- filtering: `!./lib` matches one project; use `!./lib/**`.
- git branch lockfiles: every non [A-Za-z0-9_.-] character becomes `!`,
  the name is lowercased, merging deletes branch lockfiles, "or" patterns.
- git-worktrees: remove the nonexistent worktree:new and shell/wt.sh.
- global-packages: orphaned dirs are cleaned by add -g (and update -g in
  v12), not store prune; document that add -g replaces every group that
  contains the package.
- how-peers-are-resolved: current peer directory names.
- completion: built-in completion already covers --filter and scripts.
- feature-comparison: broken JSR anchor.

Wrong claims (v12 only):
- cargo: cargo.indexUrl fails the parse, overrides apply to adding only,
  merge the duplicated Cargo index section into Registries.
- global-virtual-store: pnx keeps its own virtual store (dlx part only;
  the global-install claims are left for a separate decision).
- installation: the npm package installs on Node.js 18, only get-pnpm
  needs 22.13; trim unverified compatibility rows.

Missing facts (v12): agent-skills group filtering and naming, catalogs
(catalogs.default, pnx, --save-catalog, error codes), cargo recursive
add, build-std, resolution keys, completion zsh/pwsh setup, config
dependency location and error codes, include error codes, CI opt-outs and
pnpm ci, finders on list, filtering settings and --no-fail-if-no-match,
global install refusals, trust for npm-registry package managers, Rust
shims, overrides link from aliases.

Stale history (v12): drop v11/v10 version markers and "since v11"
narration in continuous-integration, filtering, finders, git-worktrees,
global-packages, global-virtual-store, how-peers-are-resolved, lockfile,
completion, configuring.

Review follow-up: the Node.js compatibility table keeps the pnpm 10
column; Docker Example 3 builds with all dependencies in a build stage
and ships only production dependencies, with the pnpm-workspace.yaml
caveat (both versions); catalogs keep the bare-path form; v11 global
packages name `pnpm update -g` as cleaning orphans too; the cargo
build-std note says when rust-src must be listed.

Second review: build-std needs a non-empty list; the Docker fetch layer
also depends on the project settings; branch lockfile names keep only
ASCII characters; the conflict merge needs both sides to parse; Rust
shims and package managers are default globalShims participants;
pnpm self-update also cleans orphaned global groups; v12 pnpm login
writes tokens to config.yaml; legacyDirFiltering keeps the subtree
wording (the v12 mismatch is pnpm/tasks#196); the v12 Docker
example copies the files that `include` names before pnpm fetch.
2026-10-23 02:16:47 +02:00

8.2 KiB

id, title
id title
docker Working with Docker

:::note

It is impossible to create reflinks or hardlinks between a Docker container and the host filesystem during build time. The next best thing you can do is using BuildKit cache mount to share cache between builds. Alternatively, you may use podman because it can mount Btrfs volumes during build time. If you use BuildKit cache mounts, keep the pnpm store cache scoped to mutually trusted builds. A store cache that can be written by an untrusted build should not be reused by trusted builds.

:::

Official pnpm base image

An official pnpm base image is published to GitHub Container Registry as ghcr.io/pnpm/pnpm. It is based on debian:stable-slim and contains only the pnpm standalone binary. Node.js is not bundled. This lets you pick the Node.js version yourself (inside your Dockerfile or at runtime) instead of being locked to whatever Node version a base image ships with.

Tags

Tag Meaning
<version> Exact, immutable (e.g. 11.0.0). Includes prereleases.
<major> Tracks the latest stable release within that major (e.g. 11).
latest Highest stable pnpm version. Not updated for prereleases.

Supported platforms: linux/amd64, linux/arm64.

Installing Node.js

Let pnpm install Node.js automatically from devEngines.runtime in your package.json:

{
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "22.x",
      "onFail": "download"
    }
  }
}

This example assumes the project has a pnpm-workspace.yaml. If it does not, use COPY package.json pnpm-lock.yaml ./ for the first copy step.

FROM ghcr.io/pnpm/pnpm:11
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
CMD ["pnpm", "start"]

If your project has a pnpm-workspace.yaml, copy it before running pnpm install. It contains project settings and, in a workspace, the package patterns. Omitting it can change how dependencies are installed or make a frozen install fail. Omit it from the COPY instruction only if your project has no such file.

When to use this image

  • You want the Node.js version to be pinned by your project via devEngines.runtime rather than by the base image.
  • You want to upgrade pnpm and Node.js independently.
  • You prefer a minimal Debian base without the Node.js build toolchain.

The recipes further down this page start from this image and let pnpm install Node.js. If you prefer your own Node.js base image, keep the rest of each recipe and install pnpm into that image instead.

Minimizing Docker image size and build time

  • Use a small image, e.g. ghcr.io/pnpm/pnpm or node:XX-slim.
  • Use multi-stage if possible and makes sense.
  • Use BuildKit cache mounts.

The recipes below use the official pnpm image, which already sets PNPM_HOME=/pnpm and puts /pnpm/bin on PATH, so the default store is at /pnpm/store.

When the base image has a runtime installed with pnpm runtime set node 24 -g, keep the build cache at a separate location, such as /var/cache/pnpm. Mounting a cache over /pnpm/store hides the managed runtime for the duration of the RUN instruction. The examples below set --store-dir to the cache mount.

Example 1: Build a bundle in a Docker container

Since devDependencies is only necessary for building the bundle, pnpm install --prod will be a separate stage from pnpm install and pnpm run build, allowing the final stage to copy only necessary files from the earlier stages, minimizing the size of the final image.

node_modules
.git
.gitignore
*.md
dist
FROM ghcr.io/pnpm/pnpm:11 AS base
RUN pnpm runtime set node 24 -g
COPY . /app
WORKDIR /app

FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/var/cache/pnpm pnpm install --store-dir /var/cache/pnpm --prod --frozen-lockfile

FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/var/cache/pnpm pnpm install --store-dir /var/cache/pnpm --frozen-lockfile
RUN pnpm run build

FROM base
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
EXPOSE 8000
CMD [ "pnpm", "start" ]

Example 2: Build multiple Docker images in a monorepo

Assuming you have a monorepo with 3 packages: app1, app2, and common; app1 and app2 depend on common but not each other.

You want to save only necessary dependencies for each package, pnpm deploy should help you with copying only necessary files and packages.

./
├── Dockerfile
├── .dockerignore
├── .gitignore
├── packages/
│   ├── app1/
│   │   ├── dist/
│   │   ├── package.json
│   │   ├── src/
│   │   └── tsconfig.json
│   ├── app2/
│   │   ├── dist/
│   │   ├── package.json
│   │   ├── src/
│   │   └── tsconfig.json
│   └── common/
│       ├── dist/
│       ├── package.json
│       ├── src/
│       └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
packages:
  - 'packages/*'
syncInjectedDepsAfterScripts:
- build
injectWorkspacePackages: true
node_modules
.git
.gitignore
*.md
dist
FROM ghcr.io/pnpm/pnpm:11 AS base
RUN pnpm runtime set node 24 -g

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN --mount=type=cache,id=pnpm,target=/var/cache/pnpm pnpm install --store-dir /var/cache/pnpm --frozen-lockfile
RUN pnpm run -r build
RUN pnpm deploy --filter=app1 --prod /prod/app1
RUN pnpm deploy --filter=app2 --prod /prod/app2

FROM base AS app1
COPY --from=build /prod/app1 /prod/app1
WORKDIR /prod/app1
EXPOSE 8000
CMD [ "pnpm", "start" ]

FROM base AS app2
COPY --from=build /prod/app2 /prod/app2
WORKDIR /prod/app2
EXPOSE 8001
CMD [ "pnpm", "start" ]

Run the following commands to build images for app1 and app2:

docker build . --target app1 --tag app1:latest
docker build . --target app2 --tag app2:latest

Example 3: Build on CI/CD

On CI or CD environments, the BuildKit cache mounts might not be available, because the VM or container is ephemeral and only normal docker cache will work.

So an alternative is to use a typical Dockerfile with layers that are built incrementally, for this scenario, pnpm fetch is the best option, as it only needs the lockfile and the project settings, and the layer cache will only be lost when you change the lockfile or the project settings.

pnpm fetch only fills the virtual store. A pnpm install --offline after copying the sources links the dependencies into node_modules. The build stage installs every dependency, and a separate stage installs only the production dependencies, so the final image does not carry the dev dependencies.

This example assumes the project has a pnpm-workspace.yaml. If it does not, use COPY pnpm-lock.yaml /app/ instead. If you patched any package, also copy the patches directory before running pnpm fetch.

FROM ghcr.io/pnpm/pnpm:11 AS base
RUN pnpm runtime set node 24 -g
WORKDIR /app

FROM base AS build
COPY pnpm-lock.yaml pnpm-workspace.yaml /app/
RUN pnpm fetch
COPY . /app
RUN pnpm install --offline --frozen-lockfile
RUN pnpm run build

FROM base AS prod-deps
COPY pnpm-lock.yaml pnpm-workspace.yaml /app/
RUN pnpm fetch --prod
COPY . /app
RUN pnpm install --offline --frozen-lockfile --prod

FROM base
COPY package.json /app/
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
EXPOSE 8000
CMD [ "pnpm", "start" ]