HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 351 min read

Lockfiles & npm ci

why npm install and npm ci are NOT the same

Developer toolingdeveloper tooling / package management

The myth

"I committed package.json, so my builds are reproducible."

Actually

package.json holds ranges ("^4.17.1"), not exact versions. The exact tree that actually got installed lives in the lockfile (package-lock.json / yarn.lock / pnpm-lock.yaml). Without it — or if you ignore it — two machines can resolve the same range to different versions.

Example range:

"express": "^4.17.1"

^ means "any 4.x ≥ 4.17.1". Six months later a fresh install might pull 4.19.2. Your CI passes, your teammate's fails, and nothing in package.json changed.

What the lockfile does

  • Pins every dependency AND every transitive dependency to an exact version + integrity hash.
  • Records the resolved tree, so installs can skip re-resolving.

Commit it. Always — apps and libraries alike (yes, libraries too, for your CI's sake).

The gotcha: install mutates

npm install is allowed to update the lockfile — add a missing sub-dep, re-resolve a range, "fix" it. Run it in CI and you may silently ship different versions than you tested, plus get a dirty git tree.

npm ci (clean install):

  • Requires an existing lockfile and errors if it's out of sync with package.json.
  • Deletes node_modules, installs the EXACT locked tree.
  • Never writes the lockfile.
  • Faster in CI (no resolution step).

Rule of thumb:

  • Local dev, adding a dep → npm install
  • CI / Docker / production build → npm ci

Concrete difference

# Dev machine, Jan
npm install   # resolves ^4.17.1 → 4.17.1, writes lockfile
# CI, June (lockfile committed)
npm ci        # installs exactly 4.17.1, hash-verified, ignores newer 4.19.x

Same input, same output. That's the whole point.

Common mistakes

  • .gitignore-ing the lockfile "because it's generated."
  • Using npm install in Dockerfiles → non-reproducible images.
  • Hand-editing the lockfile (regenerate instead).
  • Mixing package managers (npm + yarn) → two competing lockfiles fighting.

Your exercise

Run git status right after npm install in a repo — did package-lock.json change? Now bump a version in package.json WITHOUT updating the lock, and run npm ci. It should refuse to install. That error is the feature: it's catching the drift you'd otherwise ship.