Safeguard
Developer Tools•

Should a Dockerfile use npm ci or npm install?

Use npm ci in Dockerfiles. It installs exactly what the lockfile says and fails on a mismatch, so builds are repeatable. Here are the steps and the traps.

Safeguard Security Team
3 min read

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.

BehaviorWhy it helps an image build
Needs a lockfileYou cannot forget to commit one
Errors on mismatch instead of updatingDrift is caught in CI, not shipped
Deletes any existing node_modules firstClean install every time
Does not write package.json or lockfilesBuilds 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

  1. A missing lockfile in the build context. Check your dockerignore file does not exclude it.
  2. 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.
  3. 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.
  4. 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.