
npm ci vs npm install: One Reads Your Lockfile, the Other Rewrites It
- VersionDude
- guides
- 7 min de leitura
They look interchangeable and they are not. npm install may change package-lock.json to satisfy your ranges; npm ci refuses to, and deletes node_modules first. That single difference decides which one belongs in CI and which one belongs on your machine.
The two commands install dependencies and end with a `node_modules` directory, so it is reasonable to assume they are the same thing with different names. They are not, and the difference is not about speed even though `npm ci` is usually faster.
O que cada comando faz realmente

`npm install` treats `package.json` as the source of truth and the lockfile as a suggestion. If your `package.json` allows `^4.17.0` and version `4.18.2` exists, install is entitled to fetch it and to rewrite `package-lock.json` to say so. That is not a bug, it is the documented job of the command: reconcile what the lockfile says with what the manifest permits.
`npm ci` treats the lockfile as the source of truth and refuses to touch it. It reads `package-lock.json`, installs exactly those versions, and if the lockfile and the manifest disagree it stops with an error instead of resolving the disagreement. It also deletes `node_modules` entirely before starting, rather than reconciling what is already there.
That last detail is the one people underestimate. A partially stale `node_modules`, left over from a branch switch or an interrupted install, produces failures that survive a retry and disappear when a colleague clones the repository fresh. `npm ci` removes that whole category by never starting from an existing tree.
Which is why the rule is boring and worth following: `npm ci` in continuous integration and in Docker builds, `npm install` on your own machine when you are deliberately adding or updating a dependency.
Troca-los, e o que isso custa
The failure mode of getting this backwards is specific. Running `npm install` in CI means your pipeline can quietly install a version nobody chose, on a Tuesday, because an upstream package published a patch release that your range allows. The build that passed this morning and fails this afternoon on identical code is almost always this.
- npm ci needs a committed package-lock.json and fails without one
- npm ci deletes node_modules before installing, npm install reconciles it
- npm install can rewrite the lockfile, npm ci never does
- A lockfile and manifest that disagree stop npm ci and are silently fixed by npm install
- In Docker, copy package.json and package-lock.json before running npm ci to keep the layer cached
Running `npm ci` locally is harmless but occasionally annoying, because it wipes `node_modules` every time and refuses to proceed if you have edited `package.json` without regenerating the lock. That refusal is the feature: it is telling you the two files no longer agree.
`npm ci` requires a lockfile and will not create one. On a repository where `package-lock.json` is missing or gitignored, the command fails immediately. If that is your situation, the problem is not the command, it is that the lockfile is not committed, and nothing about your builds is reproducible until it is.
Docker, e a camada sempre reconstruida
One practical consequence for Docker: because `npm ci` needs only `package.json` and `package-lock.json`, copying just those two files before running it lets the layer cache survive any change to your source code. Copy the whole project first and every edit invalidates the install layer.
A word on `--omit=dev`, which replaced `--production`. It applies to both commands and removes development dependencies from the installed tree. In a multi-stage Docker build it belongs in the final stage, not in the stage that runs your tests, which obviously need the test runner.
O resumo honesto
The honest summary is that this is not a performance question. `npm ci` is faster in most pipelines, but the reason to use it is that it produces the same tree today and in six months from the same lockfile. Speed is a side effect of doing less work, and doing less work is exactly what makes it reproducible.
FAQ
Is npm ci faster than npm install?
Usually yes, because it skips dependency resolution entirely and installs the exact tree the lockfile describes. The gap is largest in CI, where node_modules starts empty anyway. Speed is a consequence of doing less work, not the reason to prefer it.
Can I use npm ci without a package-lock.json?
No. The command exists to install what the lockfile says, so with no lockfile there is nothing to install from and it fails immediately. If your repository has no committed lockfile, that is the thing to fix first, because no build is reproducible without it.
Does npm install always modify package-lock.json?
No, only when it needs to. If every installed version already satisfies the manifest and matches the lockfile, nothing changes. It modifies the lockfile when your ranges allow a newer version than the one pinned, or when package.json has been edited since the lock was generated.
Which one should I use in a Dockerfile?
npm ci, and copy only package.json and package-lock.json before running it so the install layer stays cached when your source changes. Add --omit=dev in the final stage, never in a stage that runs tests.
Why did my CI build break without any code change?
The most common cause is npm install in the pipeline combined with a caret or tilde range: an upstream package published a new version your range permits, and it was installed. npm ci removes this by construction, because it installs only what the lockfile pins.



A word on `--omit=dev`, which replaced `--production`. It applies to both commands and removes development dependencies from the installed tree. In a multi-stage Docker build it belongs in the final stage, not in the stage that runs your tests, which obviously need the test runner.