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](https://www.deployhq.com/blog/choosing-the-right-package-manager-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](https://www.deployhq.com/blog/what-is-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](https://www.deployhq.com/blog/what-is-ci-cd). Want it running against your own repository? [Start a](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) [build pipeline](https://www.deployhq.com/features/build-pipelines) 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](https://www.deployhq.com/deploy-from-github) or [GitLab](https://www.deployhq.com/deploy-from-gitlab), [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com/blog/using-nodejs-and-npm-with-deployhq-build)[DeployHQ](https://www.deployhq.com) build pipeline. And if your build pulls packages from a private registry or repo, the credential patterns in [private repository dependencies](https://www.deployhq.com/blog/building-your-site-using-dependencies-from-a-private-repository) keep `npm ci` working without leaking secrets.

For [agencies](https://www.deployhq.com/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 builds](https://www.deployhq.com)** — [DeployHQ](https://www.deployhq.com) runs `npm ci` in a clean environment on every push. See [pricing](https://www.deployhq.com/pricing) for team plans.

* * *

Questions? Reach out at [support@deployhq.com](mailto:support@deployhq.com) or on [X (@deployhq)](https://x.com/deployhq).

