npm ci vs npm install: Which to Use in Your Build Pipeline

Devops & Infrastructure, Node, and Tips & Tricks

npm ci vs npm install: Which to Use in Your Build Pipeline

Every Node.js project gives you two ways to install dependencies: npm install and npm ci. On your laptop they feel interchangeable — both leave you with a working node_modules. But in a build pipeline, the difference is the line between a deploy that's reproducible and one that quietly ships different code than you tested.

This guide explains what separates the two commands, why npm ci is the right default for CI/CD and production builds, and how the lockfile underneath them makes reproducible deployments possible. If you're still choosing a package manager, start with our comparison of npm vs Yarn vs pnpm vs Bun.

The Short Answer

Use npm install Use npm ci
Where Local development CI/CD pipelines, production builds
Lockfile May update it Reads it strictly, never writes
node_modules Updates in place Deletes and reinstalls clean
If lockfile is out of sync Silently updates it Fails the build
Speed Slower Faster

If you take nothing else away: use npm install while you're developing and adding packages; use npm ci everywhere a machine builds your code.

npm install vs npm ci: The Core Difference

Both commands read package-lock.json. The difference is what they're allowed to do with it.

npm install treats the lockfile as a starting point. It resolves the version ranges in your package.json, and if a newer version now satisfies a range, it installs that version and rewrites the lockfile to match. That's exactly what you want while developing — you're adding and updating packages, and the lockfile should follow.

npm ci (short for clean install) treats the lockfile as law. It:

  1. Deletes node_modules entirely before installing, so no stale state from a previous build leaks in.
  2. Installs the exact versions recorded in package-lock.json — no resolution, no negotiation.
  3. Never modifies the lockfile.
  4. Fails immediately if package-lock.json and package.json are out of sync.

That last point is the one that saves you. If a teammate bumped a dependency in package.json but forgot to commit the regenerated lockfile, npm install would paper over it by silently resolving new versions. npm ci stops the build and tells you — before an untested dependency tree reaches production.

# In your build pipeline
npm ci
npm run build

Why npm ci Is the Right Default for CI/CD

Three properties make npm ci the correct choice anywhere a machine builds your code:

  • Reproducibility. Every build — a teammate's, the CI server's, the production deploy — installs the identical dependency tree. What passed tests is what ships. This is the core of continuous integration: a build is only trustworthy if it's deterministic.
  • Speed. Because npm ci skips dependency resolution and installs straight from the lockfile, it's consistently faster than npm install on a clean machine — which is every CI run.
  • Safety. The clean node_modules wipe plus the strict lockfile check means a build can't succeed on a lie. No stale packages, no drift, no works in CI, breaks in prod.

If your pipeline still runs npm install, switching that single line to npm ci is one of the cheapest reliability upgrades you can make to a CI/CD pipeline. Want it running against your own repository? Start a DeployHQ trial and point a build pipeline at your repo — npm ci on every push, in a clean environment.

The Lockfile: What Makes npm ci Work

npm ci is only as good as the lockfile it reads. So it's worth understanding what package-lock.json actually is — and why committing it is non-negotiable.

package-lock.json is an automatically generated file that records the exact version of every package installed in your project, including every nested dependency. npm creates and updates it whenever you run npm install. Where package.json declares what your project needs, package-lock.json records what was actually installed.

package.json vs package-lock.json

Aspect package.json package-lock.json
Purpose Declares dependencies and version ranges Records exact installed versions
Version format Semver ranges (^1.2.3, ~1.2.3) Exact versions (1.2.3)
Managed by Developer npm (auto-generated)
Includes nested deps No Yes — the entire dependency tree
Should you edit it Yes No — let npm manage it

Consider this package.json entry:

{
  "dependencies": {
    "express": "^4.18.0"
  }
}

The ^4.18.0 range means any version from 4.18.0 up to (but not including) 5.0.0. Today that might resolve to 4.18.2; next month it could resolve to 4.21.0. Without a lockfile, two developers running npm install at different times get different versions. The lockfile eliminates that ambiguity — it pins every package to an exact version, a download URL, and an integrity hash.

The Works on My Machine Problem

This is the exact scenario the lockfile — enforced by npm ci — prevents:

  1. Developer A runs npm install on Monday — Express 4.18.2 is installed
  2. Developer B runs npm install on Friday — Express 4.21.0 shipped on Wednesday
  3. Developer B hits a subtle bug Developer A can't reproduce
  4. The CI server installs yet another combination

With package-lock.json committed and npm ci in the pipeline, all three environments get the same dependency tree, every time.

Always Commit Your Lockfile

Yes, always. Committing package-lock.json gives you:

  1. Reproducible builds — every developer, CI server, and deploy target gets identical dependencies
  2. Security auditing — lockfile diffs in PRs make unexpected packages visible
  3. Working npm ci — it requires a committed lockfile; without one it errors out
  4. Supply-chain protection — integrity hashes catch tampered or corrupted packages

A common .gitignore mistake is excluding package-lock.json. If it's in your .gitignore, remove it now.

The Other Package Managers Have Their Own ci Command

npm ci isn't unique — every modern package manager has a strict, lockfile-frozen install mode meant for CI. If your project uses one of these instead, reach for its equivalent in your pipeline:

Package manager Local install Strict CI install
npm npm install npm ci
Yarn Classic (v1) yarn install yarn install --frozen-lockfile
Yarn Berry (v2+) yarn install yarn install --immutable
pnpm pnpm install pnpm install --frozen-lockfile
Bun bun install bun install --frozen-lockfile

They all do the same job: install exactly what the lockfile says and fail if it's out of sync, rather than silently updating it. The principle is identical no matter which tool you standardize on.

Using npm ci in Your Deployment Pipeline

When code moves from development to production, npm ci is what guarantees the server installs what you tested. A typical DeployHQ build pipeline runs:

npm ci
npm run build

npm ci reads the committed lockfile and installs the exact dependency tree in a clean environment, which means:

  • Staging and production builds are identical
  • A passing CI build guarantees the same versions ship
  • No untested dependency can sneak into production

If you deploy from GitHub or GitLab, DeployHQ pulls your repository — including the lockfile — and runs your build commands for you. For a full walkthrough of configuring Node versions and caching, see using Node.js and npm with the DeployHQ build pipeline. And if your build pulls packages from a private registry or repo, the credential patterns in private repository dependencies keep npm ci working without leaking secrets.

For agencies managing many client projects, this determinism isn't a nicety — every deploy must produce the same output regardless of when it runs.

Common Pitfalls

  • Running npm install in CI. It may rewrite the lockfile mid-build, so production can differ from what you tested. Use npm ci.
  • A lockfile that's out of sync. If npm ci fails with a sync error, that's the feature working. Run npm install locally, verify everything works, and commit the regenerated lockfile — don't switch the pipeline to npm install to dodge the error.
  • Editing package-lock.json by hand. Never. To resolve a merge conflict, accept either side and regenerate:
git checkout --theirs package-lock.json
npm install          # regenerates from both sets of package.json changes
git add package-lock.json
git commit -m "Resolve lockfile merge conflict"
  • Committing more than one lockfile. If a repo has both package-lock.json and yarn.lock, pick one package manager and delete the other lockfile — competing lockfiles produce inconsistent installs.

FAQ

Is npm ci faster than npm install? Yes, on a clean machine. npm ci skips dependency resolution and installs straight from the lockfile, which is why it's the better fit for CI runs that always start fresh.

Can I use npm install instead of npm ci in production? You can, but you shouldn't. npm install may update the lockfile, so production could differ from what you tested. Use npm ci for all automated environments.

Does npm ci update package.json or package-lock.json? Never. npm ci only reads them. If they're out of sync, it fails rather than writing changes — which is exactly what you want in a pipeline.

Why does npm ci delete node_modules? To guarantee a clean install with no leftover state from a previous build. It's slower to wipe and reinstall than to patch in place, but on CI that clean slate is the whole point.

Why is my lockfile changing when I haven't touched package.json? Usually because teammates use different npm versions. Standardize with a .nvmrc or an engines field in package.json.

How do I know if my lockfile is out of date? Run npm ci. If it fails with a sync error, regenerate with npm install, verify everything works, and commit.


npm install and npm ci aren't competitors — they're tools for two different jobs. Install and update packages locally with npm install; let npm ci enforce that exact, tested dependency tree everywhere a machine builds your code. Commit your lockfile, wire npm ci into your pipeline, and works on my machine stops being a deployment risk.

Deploy Node.js apps with reproducible buildsDeployHQ runs npm ci in a clean environment on every push. See pricing for team plans.


Questions? Reach out at support@deployhq.com or on X (@deployhq).