CalcSnippets
DevOps 2 min read

`.nvmrc` Node Version Drift Between Local and CI

A repository can declare one Node version while developers, CI, and package managers quietly use three others. Here is a small workflow that makes the drift visible.

The file is not the version

Adding .nvmrc does not make every environment use that version. It only gives tools a value they may read. A developer can forget nvm use; a CI runner can ignore the file; a container can use a different base image; and Corepack can activate a package manager that behaves differently under another Node release.

Treat runtime selection as a chain with an explicit source of truth:

  1. package.json declares the supported Node range in engines.node.
  2. package.json declares the package manager in packageManager.
  3. .nvmrc or .node-version gives local version managers a convenient default.
  4. CI reads the declared version or uses the same pinned major.
  5. The container image and deployment runtime follow the same contract.

If those values disagree, installation failures are only the first symptom. Native modules may compile against a different ABI, lockfiles may change, and a successful local build can still fail in CI.

Compare the actual values

Run this at the repository root:

printf 'local node: '; node --version
printf 'local package manager: '; pnpm --version
cat .nvmrc .node-version 2>/dev/null || true
node -p "require('./package.json').engines || {}"
node -p "require('./package.json').packageManager || 'no packageManager field'"

Then compare it with the CI log. Print versions as an early, visible step rather than relying on the runner label. A label such as ubuntu-latest describes an image family, not your complete runtime contract.

Pin the useful parts

Use a major version only when the project intentionally accepts compatible updates. Pin an exact patch when a native toolchain, release build, or regulated environment needs a repeatable image. Whichever choice you make, make it consistent. A repository that says >=20 while CI uses 22 and a developer uses 18 has not defined a useful support policy.

For pnpm, keep this in package.json:

{
  "engines": { "node": ">=22 <23" },
  "packageManager": "[email protected]"
}

Use Corepack or the project-approved package-manager installation method in CI, then run pnpm install --frozen-lockfile. Do not “fix” a runtime mismatch by regenerating the lockfile in the pipeline.

The Node Version Drift Checker compares pasted local, CI, and declared values. It does not execute version managers, so use it as a review aid before making the repository contract stricter.

Keep reading

Related guides