How to Reduce Docker CVEs: 6 Steps That Work
Published July 7, 2026 · 9 min read
You scanned your image, the report says 900 findings, and your security team wants the number down. This guide walks through the six changes that actually reduce Docker CVEs, in the order of effort-to-impact, with runnable examples for each.

Where Docker CVEs actually come from
Before changing anything, understand the shape of the problem. In a typical application image, the overwhelming majority of findings are not in your code or even your dependencies — they are in the operating system packages of the base image. Every package in the base carries its own advisory history: the C library, package managers, shells, TLS libraries, compression tools.
The numbers make the point. When we scanned nginx:1.27 (Debian-based) it produced 2,952 findings; the same nginx on the Alpine base produced 619 (ScanRook v1.14.2, warm-cache scan, 2026-07-04). Same web server, 79% fewer findings, purely because the base contains less software. That asymmetry drives the order of the steps below: shrink the operating system first, then patch what remains, then keep it patched.
Step 1: Move to a smaller base image
The single highest-impact change. Every package you remove from the image is a package that can never appear in a scan report again. For most runtimes there are three tiers: the full distribution image, a slim/Alpine variant, and distroless.
# Before: full Debian base, complete GNU userland FROM node:22 # Better: Debian slim — drops docs, most tooling FROM node:22-slim # Better still: Alpine — musl libc + BusyBox userland FROM node:22-alpine # Smallest: distroless — no shell, no package manager FROM gcr.io/distroless/nodejs22-debian12
Each tier trades convenience for surface: Alpine swaps glibc for musl (a compatibility concern for native modules), and distroless removes the shell entirely (a debugging concern). Our Alpine vs Debian vs Distroless comparison covers the tradeoffs in depth. If you can only make one change from this article, make this one.
Base image choice, measured — ScanRook v1.14.2, 2026-07-04
Step 2: Rebuild with a fresh base — regularly
Base images are rebuilt upstream as patches land, but your image is frozen at the moment you built it. An image built six months ago is carrying six months of advisories that are already fixed upstream. Two flags matter:
# --pull: re-fetch the base image even if cached locally # --no-cache: rebuild every layer so package installs re-run docker build --pull --no-cache -t myapp:$(date +%Y%m%d) .
Put this on a schedule — weekly is a reasonable default, nightly for internet-facing services. A scheduled CI job that rebuilds, scans, and redeploys is the difference between a CVE count that decays continuously and one that only improves when someone remembers.
Step 3: Apply OS security updates in the build
Even a freshly pulled base can lag the distribution's security feed by days. Applying updates during the build closes that gap:
# Debian/Ubuntu bases
RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Alpine bases
RUN apk upgrade --no-cacheThe cleanup lines matter twice over: they shrink the image, and removing the APT list cache means stale metadata cannot linger in a layer. If your organization requires byte-reproducible builds, pin the base by digest and treat the deliberate rebuild in Step 2 as the update mechanism instead.
Step 4: Use multi-stage builds to drop the toolchain
Compilers, dev headers, git, curl, and build tools are prime CVE carriers, and none of them belong in the final image. Multi-stage builds let you compile in a fat image and ship from a thin one:
# Stage 1: build with the full toolchain FROM golang:1.23 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server # Stage 2: ship only the artifact FROM gcr.io/distroless/static-debian12 COPY --from=build /out/server /server USER nonroot:nonroot ENTRYPOINT ["/server"]
The final image here contains one binary and CA certificates — no compiler, no package manager, no shell. The same pattern works for Node (build stage runs npm ci and bundling, runtime stage copies dist/ and production node_modules) and for Python with wheels built in the first stage.
Step 5: Stop installing what you do not need
Debian's package manager installs “recommended” packages by default, which quietly pulls in software you never asked for — each with its own advisory history. Disable that, and audit what you install explicitly:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
libpq5 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*Common stowaways worth removing: curl and wget (use build-stage downloads instead), openssh clients, perl and python pulled in as dependencies of tooling, and debug utilities added “temporarily” during an incident. If a package does not execute in production, it is pure scan-report liability.
Step 6: Update your application dependencies
After the OS layer is under control, what remains is your dependency tree — npm, pip, Go modules, Maven. These findings are usually the most actionable, because fixes are published as ordinary version bumps:
# Node: report, then apply non-breaking fixes npm audit npm audit fix # Python: upgrade pinned requirements deliberately pip list --outdated pip install -U <package>==<fixed-version> # Go: patch-level module updates go get -u=patch ./... && go mod tidy
Update lockfiles in the same commit as the rebuild so the scan result maps cleanly to a single change. Automated dependency-update tooling (Renovate, Dependabot) turns this step into review work instead of research work.
Verifying it worked
Measure, do not assume. Scan the image before your changes, apply the steps, and scan again with the same scanner version:
curl -fsSL https://scanrook.io/install.sh | sh docker save myapp:before -o before.tar scanrook scan --file before.tar --format json --out before.json docker save myapp:after -o after.tar scanrook scan --file after.tar --format json --out after.json # Compare severity totals jq '.summary' before.json after.json
Expect the distribution of what remains to shift, too: after Steps 1–5 the leftover findings should be concentrated in your application dependencies (fixable via Step 6) and a small tail of no-fix-available OS advisories, which are triage candidates rather than fix candidates. If a finding survives every step, verify whether the package is even reachable at runtime — our piece on installed-state scanning explains how scanners decide what is really present.
Where ScanRook fits
Reducing CVEs is a loop, not a project: rebuild, scan, compare, fix, repeat. ScanRook is built for that loop — scan a saved image tar locally or in CI, get JSON with severity totals you can gate builds on, and use confidence tiers to separate findings in packages that are verifiably installed from heuristic matches. Start with the scanning guide or the docs for CI recipes.
Frequently asked questions
What is the fastest way to reduce CVEs in a Docker image?
Switch to a smaller base image. It removes hundreds of findings in one change because most findings live in OS packages you never use.
Does rebuilding an image reduce vulnerabilities?
Yes — your image only picks up base patches when you rebuild with --pull. Schedule it weekly at minimum.
Should I run apt-get upgrade in my Dockerfile?
For security patches, yes. If you need reproducible builds, pin the base by digest and rely on scheduled rebuilds instead.
Why do CVEs remain after updating everything?
Some advisories have no released fix, and some packages will not be patched on older distribution releases. Triage those by reachability, or move to a base that does not ship the package.
Measure the reduction with ScanRook
Scan before-and-after builds, diff severity totals, and gate CI on the result. ScanRook matches every installed package against OSV, NVD, and vendor advisory data so the number you report to your security team is one you can defend.