Best practices

How to Fix npm Vulnerabilities in Docker with npm audit fix

Published August 4, 2026 · 9 min read

A Node image scan usually splits findings into two buckets: OS packages from the base image, and npm packages from your dependency tree. This guide focuses on the second bucket — fixing npm vulnerabilities in Docker builds with npm audit fix and a disciplined rebuild, without breaking reproducible installs or accidentally shipping devDependencies.

Fixing npm vulnerabilities in Docker builds

Why npm findings need a different fix path than OS findings

OS package findings get fixed by rebuilding against a patched base image. npm findings live in your lockfile, which only your team controls — there is no upstream maintainer who will silently patch it for you. Fixing them means updating package-lock.json deliberately and rebuilding the image against the updated file.

Step 1: Audit the lockfile outside the container first

Run the audit locally or in CI against the lockfile directly, before touching the Dockerfile at all:

npm audit --omit=dev

# JSON output for scripting or CI gating
npm audit --omit=dev --json > audit.json
jq '.metadata.vulnerabilities' audit.json

--omit=dev matters here: findings in devDependencies like test runners and bundlers will not ship in a properly built image, so auditing production dependencies only keeps the report focused on what actually matters for the container.

Step 2: Apply non-breaking fixes with npm audit fix

Update the lockfile locally and commit the result — do not run this inside the Dockerfile, where the change would never leave the container:

npm audit fix
git diff package-lock.json

# Run tests against the updated lockfile before committing
npm test

git add package-lock.json
git commit -m "chore: apply npm audit fixes"

Step 3: Handle findings that need a major version bump

npm audit fix skips fixes that require a breaking semver change. Review those individually rather than forcing them blind:

# See what --force would change before running it
npm audit fix --dry-run --force

# Or bump one package deliberately and test
npm install <package>@latest
npm test

Treat --force as a last resort applied to one package at a time with a full test run after, not a blanket command to run whenever the audit looks stubborn.

Step 4: Build with npm ci so the container matches the lockfile exactly

npm install can silently drift from the lockfile inside a build; npm ci installs exactly what is pinned and fails loudly if the lockfile is out of sync:

FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

Step 5: Exclude devDependencies from the shipped image

Test frameworks, linters, and bundlers do not need to exist in the running container. Use a multi-stage build so the final image installs production dependencies only:

FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim AS runtime
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]

See our broader guide to reducing CVEs in Docker images for how this pattern combines with base image choice.

Step 6: Handle vulnerable transitive dependencies

Sometimes the vulnerable package is not one you installed directly but a dependency of a dependency that has not released a compatible update. Use overrides to force a specific version:

{
  "name": "myapp",
  "overrides": {
    "vulnerable-transitive-package": "^2.1.4"
  }
}

Run npm install once to regenerate the lockfile with the override applied, then run your test suite — an override skips normal dependency resolution, so verify the forced version is actually compatible with the packages that depend on it.

Verifying the fixes landed in the image

An updated lockfile does not guarantee the built image reflects it — confirm with a scan of the actual image, not just the audit output:

docker build --no-cache -t myapp:patched .
docker save myapp:patched -o myapp.tar

curl -fsSL https://scanrook.io/install.sh | sh
scanrook scan --file myapp.tar --format json --out report.json
jq -r '.findings[] | select(.package.ecosystem=="npm") | "\(.severity)\t\(.package.name)\t\(.package.version)"' report.json

If a finding still appears after these steps, confirm it is not coming from devDependencies leaking into the image via a missed COPY node_modules in an earlier build stage — that is the most common reason a fixed lockfile does not translate into a clean scan.

Where ScanRook fits

ScanRook reads the packages genuinely installed in the built image — not just what the manifest declares — so it catches cases where devDependencies or stale node_modules slipped past a lockfile fix. Combine it with installed-state scanning and see the docs for CI setup.

Frequently asked questions

How do I fix npm vulnerabilities in a Docker image?

Run npm audit fix against the lockfile, rebuild with npm ci, and exclude devDependencies from the final image.

Should npm audit run inside the Dockerfile?

No — run it in CI against the lockfile and commit the fix, so the update is a reviewable diff instead of a silent in-container change.

Does npm ci fix vulnerabilities?

No, it installs exactly what the lockfile specifies. You still need npm audit fix or a manual bump to update the lockfile first.

Do devDependencies show up in an image scan?

Only if they are actually installed in the scanned image — a multi-stage build with --omit=dev excludes them entirely.

Confirm your npm fixes with ScanRook

Scan the built image, not just the lockfile, to catch devDependencies or stale node_modules that a manifest-only check would miss.

Related Posts

More on this topic.