Same commit, different build: lockfile troubleshooting runbook
For developers debugging why identical source commits install different dependency trees on different machines or in CI. This runbook shows the fastest checks, exact commands, and concrete fixes for lockfile drift, package-manager mismatches, platform-specific resolution, and registry/cache differences.
TL;DR — If the same commit builds differently on two machines, start by comparing the package manager version, the lockfile actually used, and whether the install command honors the lockfile (
npm ci,yarn install --immutable,pnpm install --frozen-lockfile,bundle install --deployment,pip-sync). The single most common fix is: commit the lockfile, pin the package-manager/runtime version, clear caches, and rerun with the strict "frozen/immutable" install mode so drift fails fast instead of silently re-resolving. Reading time: ~6 min
The scenario
You merge a harmless-looking frontend change on a Tuesday afternoon. CI passes on Linux, but your teammate on macOS gets a TypeScript error from a dependency you did not touch, and the production Docker build now fails with a different transitive package version than your laptop installed an hour ago. You are on the same Git commit, git status is clean on both machines, and yet node_modules, vendor/bundle, or the Python environment are not the same. Someone says "works on my machine," and now you need to prove exactly why.
Symptoms
- Different dependency versions from the same commit:
$ npm ls lodash
app@1.0.0 /workspace/app
└── lodash@4.17.21
$ npm ls lodash
app@1.0.0 /Users/dev/app
└── lodash@4.17.20
- Lockfile mutation during install:
$ npm install
added 842 packages, and audited 843 packages in 12s
2 packages are looking for funding
run `npm fund` for details
$ git diff -- package-lock.json
- Strict install fails in CI but not locally:
$ npm ci
npm ERR! code EUSAGE
npm ERR! npm ci can only install packages when your package.json and package-lock.json are in sync
$ yarn install --immutable
YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
$ pnpm install --frozen-lockfile
ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with package.json
- Different package-manager versions produce different lockfile formats:
$ npm -v
10.8.2
$ npm -v
9.9.4
- Platform-specific optional/native dependency behavior:
npm ERR! code EBADPLATFORM
npm ERR! notsup Unsupported platform for fsevents@2.3.3: wanted {"os":"darwin"} (current: {"os":"linux"})
prebuild-install WARN install No prebuilt binaries found (target=22.11.0 runtime=node arch=arm64 libc= platform=linux)
node-gyp ERR! build error
- Registry/source mismatch:
$ npm config get registry
https://registry.npmjs.org/
$ npm config get registry
https://artifactory.example.com/api/npm/npm/
- Python/Ruby equivalents:
$ pip install -r requirements.txt
Collecting urllib3<3,>=1.21.1
Using cached urllib3-2.3.0-py3-none-any.whl
$ bundle install
Your lockfile was created by an old Bundler that left some things out.
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...
Likely causes
| Cause | How common | Quick check |
|---|---|---|
| Non-strict install command re-resolved dependencies instead of honoring the lockfile | Very common | grep -R "npm install|yarn install|pnpm install|pip install -r|bundle install" .github .gitlab-ci.yml Dockerfile* package.json |
| Lockfile missing, uncommitted, or out of sync with manifest | Very common | git status --short && git diff -- package-lock.json yarn.lock pnpm-lock.yaml Gemfile.lock poetry.lock requirements*.txt |
| Different package-manager version or lockfile format | Very common | node -v && npm -v && yarn -v && pnpm -v && python --version && pip --version && ruby --version && bundle --version |
| Different registry, mirror, auth scope, or offline cache | Common | npm config get registry; yarn config get npmRegistryServer; pnpm config get registry; pip config list; bundle config |
| Platform/runtime-specific resolution (OS, CPU, libc, Node/Python/Ruby version) | Common | `uname -srm; arch; ldd --version 2>/dev/null |
| Postinstall scripts / native builds / optional deps changed output | Less common | npm config get ignore-scripts; grep -R 'postinstall|install|optionalDependencies' package.json |
Step-by-step diagnosis
- Compare the exact toolchain on both machines.
node -v && npm -v && yarn -v && pnpm -v
python --version && pip --version
ruby --version && bundle --version
uname -srm; arch
If versions differ, especially major versions, this is likely your problem. npm 9 vs 10, Yarn 1 vs 4, pnpm 8 vs 9, Bundler 2.4 vs 2.5, or Python 3.11 vs 3.12 can produce different resolution or lockfile behavior. Jump to Fixes → Different package-manager version or lockfile format.
- Check whether the lockfile exists, is committed, and matches the manifest.
git ls-files package-lock.json yarn.lock pnpm-lock.yaml Gemfile.lock poetry.lock requirements*.txt
git status --short
git diff -- package.json package-lock.json yarn.lock pnpm-lock.yaml Gemfile Gemfile.lock pyproject.toml poetry.lock requirements*.txt
If the lockfile is untracked, modified, or absent for a package manager that expects one, this is your problem. If package.json changed without the lockfile changing, or vice versa, jump to Fixes → Lockfile missing, uncommitted, or out of sync with manifest.
- Verify the install command is strict.
grep -R "npm install\|yarn install\|pnpm install\|pip install -r\|bundle install" .github .gitlab-ci.yml Dockerfile* package.json Makefile . 2>/dev/null
Problem indicators:
npm installin CI/Docker instead ofnpm ciyarn installwithout--immutableon Yarn Berrypnpm installwithout--frozen-lockfilepip install -r requirements.txtwhererequirements.txtis not fully pinnedbundle installwithout deployment/frozen settings in CI Jump to Fixes → Non-strict install command re-resolved dependencies.
- Compare registries and package source config.
npm config get registry; npm config get @your-scope:registry
yarn config get npmRegistryServer 2>/dev/null
pnpm config get registry
pip config list 2>/dev/null
bundle config
If one machine points at a private mirror and the other points at the public registry, or one has auth for a scoped package and the other does not, resolution can differ. Jump to Fixes → Different registry, mirror, auth scope, or offline cache.
- Check for platform/runtime-specific packages and native builds.
uname -srm; arch
node -p "process.platform+' '+process.arch+' '+process.version"
grep -R 'optionalDependencies\|os\|cpu\|postinstall\|install' package.json .
If one machine is darwin arm64 and the other is linux x64, or one is Alpine/musl and the other is Debian/glibc, optional/native dependencies can differ. If logs show EBADPLATFORM, node-gyp, prebuild-install, or missing binary artifacts, jump to Fixes → Platform/runtime-specific resolution or Fixes → Postinstall scripts / native builds / optional deps.
- Reproduce in a clean environment with cache disabled or cleared.
⚠️ Clearing caches slows the next install and may increase network traffic. It should not destroy source data, but do not run destructive cleanup in a shared build agent without checking ownership and disk policies.
rm -rf node_modules .pnpm-store .yarn/cache vendor/bundle .venv
npm cache verify || true
pnpm store prune || true
yarn cache clean || true
pip cache purge 2>/dev/null || true
bundle clean --force 2>/dev/null || true
Then rerun the strict install for your stack. If the clean install now matches CI, the issue was stale cache or prior local mutation; still fix the root cause in CI and repo config.
Fixes
Non-strict install command re-resolved dependencies instead of honoring the lockfile
Use the strict install mode everywhere non-interactive: CI, Docker, deploy hooks.
# npm
npm ci
# Yarn Berry/4+
yarn install --immutable
# pnpm
pnpm install --frozen-lockfile
# Bundler
bundle config set deployment true
bundle config set frozen true
bundle install
# Python: use a fully pinned lock/constraints workflow
pip install -r requirements.txt --require-hashes
# or
pip-sync requirements.txt
Docker example:
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts=false
Verify it worked:
git diff --exit-code -- package-lock.json yarn.lock pnpm-lock.yaml Gemfile.lock poetry.lock requirements*.txt
Lockfile missing, uncommitted, or out of sync with manifest
Regenerate the lockfile with the project’s intended package-manager version, then commit both manifest and lockfile together.
# npm
rm -rf node_modules package-lock.json
npm install
git add package.json package-lock.json
# Yarn
rm -rf node_modules yarn.lock
yarn install
git add package.json yarn.lock
# pnpm
rm -rf node_modules pnpm-lock.yaml
pnpm install
git add package.json pnpm-lock.yaml
# Bundler
bundle lock
git add Gemfile Gemfile.lock
If CI should reject drift, add a check:
git diff --exit-code -- package-lock.json yarn.lock pnpm-lock.yaml Gemfile.lock poetry.lock requirements*.txt
Verify it worked:
npm ci || yarn install --immutable || pnpm install --frozen-lockfile
Different package-manager version or lockfile format
Pin both runtime and package-manager versions in repo config.
{
"packageManager": "pnpm@9.15.0",
"engines": {
"node": "22.11.x"
}
}
# Node version pinning
printf '22.11.0\n' > .nvmrc
# or
printf '22.11.0\n' > .node-version
# Corepack for Yarn/pnpm
corepack enable
corepack use pnpm@9.15.0
# or
corepack use yarn@4.6.0
Bundler example:
bundle _2.5.22_ install
Python example:
python3.12 -m venv .venv
. .venv/bin/activate
python --version
Verify it worked:
node -v && corepack pnpm -v
Different registry, mirror, auth scope, or offline cache
Normalize registry config in repo or CI, not just in a developer home directory.
# npm / pnpm
printf 'registry=https://registry.npmjs.org/\n' > .npmrc
printf '@your-scope:registry=https://registry.npmjs.org/\n' >> .npmrc
# Yarn Berry
printf 'npmRegistryServer: "https://registry.npmjs.org"\n' > .yarnrc.yml
If you use an internal mirror, point every environment to the same URL and configure auth via CI secrets, not ad hoc local config. Check for stale cache after changing registries:
npm cache clean --force
pnpm store prune
yarn cache clean
Verify it worked:
npm config get registry; pnpm config get registry; yarn config get npmRegistryServer 2>/dev/null
Platform/runtime-specific resolution (OS, CPU, libc, Node/Python/Ruby version)
Build in the same target environment you deploy. For Node apps, prefer containerized installs for Linux deploy targets.
FROM node:22-bookworm
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
If optional deps are expected to differ by OS, do not compare raw node_modules trees between macOS and Linux; compare lockfile integrity and app build output. For Alpine vs Debian issues, switch to a glibc-based image if native modules only ship glibc prebuilds.
Verify it worked:
docker build --no-cache -t app-repro .
Postinstall scripts / native builds / optional deps changed output
Inspect and control scripts explicitly.
npm config get ignore-scripts
npm ci --foreground-scripts
If a package compiles native code during postinstall, pin the runtime and compiler image, or disable scripts only if the package supports it.
# Example: fail closed in CI if scripts should not run
npm ci --ignore-scripts
For optional deps causing cross-platform noise, keep them optional and test in the deploy OS. Do not promote a macOS-only package into required dependencies for a Linux deploy. Verify it worked:
npm ls --depth=0 && npm run build
Prevention
- Pin the package manager and runtime in-repo so every machine resolves with the same tooling.
{
"packageManager": "yarn@4.6.0",
"engines": { "node": "22.11.x" }
}
printf '22.11.0\n' > .nvmrc
corepack enable
- Enforce strict installs in CI and Docker.
npm ci
# or
pnpm install --frozen-lockfile
# or
yarn install --immutable
- Add a CI job that fails if install mutates the lockfile.
npm ci
git diff --exit-code -- package-lock.json
- Standardize registry config in the repo, not per-user dotfiles.
printf 'registry=https://registry.npmjs.org/\n' > .npmrc
- Build in the deployment OS/arch, especially when native modules are involved.
View as text
[Dev Mac] ---- 1. git push ----> [CI Linux container] [CI Linux container] ---- 2. strict install ----> [Lockfile] [CI Linux container] ---- 3. build artifact ----> [Prod Linux]
- For Python/Ruby, use lock workflows that fully pin transitive dependencies and verify hashes where supported.
pip-compile --generate-hashes -o requirements.txt pyproject.toml
pip install -r requirements.txt --require-hashes
bundle config set frozen true
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI