Use npm ci in a Dockerfile. It installs exactly what your lockfile records, fails if the lockfile and package.json disagree, and never rewrites either file. That makes every build of the same commit install the same dependency tree, which is what you want in an image.
What npm ci does differently
The npm documentation lists the behaviors that matter here.
| Behavior | Why it helps an image build |
|---|---|
| Needs a lockfile | You cannot forget to commit one |
| Errors on mismatch instead of updating | Drift is caught in CI, not shipped |
| Deletes any existing node_modules first | Clean install every time |
| Does not write package.json or lockfiles | Builds do not change your source |
Plain npm install can resolve newer versions within your ranges and update the lockfile. Inside an image that means two builds a week apart can ship different code for the same commit.
A sensible Dockerfile pattern
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
ENV NODE_ENV=production
RUN npm ci
FROM node:22-bookworm-slim
WORKDIR /app
# COPY with the from option set to deps
COPY node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]
Copying only the package files first lets the cache reuse the install layer until dependencies change. Check the exact Node tag you use against the project's support dates before you copy this.
Dev dependencies
A common worry is shipping test tools to production. The npm docs state that the omit setting defaults to dev only when NODE_ENV is set to production, and otherwise it is empty. So setting that variable, or passing the omit option for dev, is how you keep dev packages out. Do not assume npm ci drops them on its own. If you need dev tools to build, install them in a build stage and copy only the output forward.
Traps
- A missing lockfile in the build context. Check your dockerignore file does not exclude it.
- Building behind a cache that holds old packages. The install layer is reused until the package files change, so schedule rebuilds. See rebuilding images regularly.
- Install scripts running during the build. Dependencies can run scripts at install time, so keep secrets out of the build environment and use build secrets only where needed.
- A lockfile that pins vulnerable versions. A repeatable build also repeats old flaws, so scan the lockfile or the finished image.
Check what you shipped
After the build, generate a dependency inventory from the image or the lockfile and compare it with what you expected. The demo shows what a dependency inventory view looks like.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
Does npm ci update my lockfile?
No. It leaves package.json and lockfiles untouched, and errors if they are out of sync.
Will npm ci skip dev dependencies automatically?
Only if the omit setting resolves to dev, which by default happens when NODE_ENV is production. Set it deliberately.
Safeguard
Safeguard does SBOM and dependency scanning, and its policies and gates can run in CI so a bad image does not reach production. You can try a free scan before deciding anything.
Sources
Last reviewed: 19 October 2026.