Best practices

How to Patch Docker Base Image Vulnerabilities

Published July 11, 2026 · 9 min read

A scan report full of base-image CVEs is not a code problem — it is a build-process problem. This guide walks through patching Docker base image vulnerabilities in the right order: find what is actually vulnerable, pull the fix, apply OS-level updates, and lock in a cadence so the count does not creep back up.

Patching Docker base image vulnerabilities

Why base-image patches lag behind

A Docker image is not a live system — it is a frozen snapshot of packages at build time. Upstream maintainers publish patched base images continuously as advisories close, but your build only benefits from those patches the moment you rebuild. Between rebuilds, every fixed CVE upstream is still an open finding in your image.

That gap compounds. A base image pulled once at project start and never refreshed can silently accumulate a year of unpatched advisories, even though the maintainer fixed every one of them upstream. The six steps below close that gap and keep it closed.

What patching can and cannot reach — ScanRook v1.14.2, 2026-07-04

Horizontal bar chart of total ScanRook findings per image: alpine 3.20 has 301, nginx 1.27-alpine 619, ubuntu 24.04 1,365, nginx 1.27 2,952CriticalHighMediumLowNo severity assignedalpine:3.20301 total · 20 criticalnginx:1.27-alpine619 total · 84 criticalubuntu:24.041,365 total · 130 criticalnginx:1.272,952 total · 408 critical
Rebuilding narrows the gap; the base you rebuild onto sets it. nginx:1.27 reports 2,952 total findings (408 critical) against 619 (84 critical) for nginx:1.27-alpine — the same server, a smaller base. ubuntu:24.04 reports 1,365 (130 critical) and alpine:3.20 reports 301 (20 critical) with no application on top at all, which is the floor the steps below are patching toward. If a rebuilt image is still reporting thousands of findings, the remaining lever is the base image, not another patch pass. Bar length is linear in total findings, so the smallest bars are only a few pixels wide — exact totals are printed at right. The four rated buckets do not always add up to the total because some advisories carry no CVSS severity; that remainder is the unfilled part of each bar.

Step 1: Identify which base-image CVEs are actually present

Before patching anything, scan the current image and export the report. Patching blind wastes effort on packages that were never vulnerable in the first place.

curl -fsSL https://scanrook.io/install.sh | sh

docker save myapp:current -o myapp-current.tar
scanrook scan --file myapp-current.tar --format json --out report.json

# List findings by severity and package
jq -r '.findings[] | "\(.severity)\t\(.package.name)\t\(.cve)"' report.json | sort

Group the output by package. Findings clustered in a handful of OS packages (openssl, libc, zlib) are almost always base-image findings, not application findings — that is your patch target for the rest of this guide.

Step 2: Rebuild against a fresh pull of the base tag

The single most common reason a base-image CVE stays open is that the image was never rebuilt against a current pull of the tag. Force Docker to check upstream instead of reusing the cached layer:

# --pull: re-check the registry for a newer image under the same tag
# --no-cache: rebuild every layer, so package installs re-run against the new base
docker build --pull --no-cache -t myapp:patched .

This alone resolves any CVE that the base-image maintainer has already fixed and published under the tag you use. It is free — no Dockerfile changes required — and it is the first thing to try.

Step 3: Apply OS security updates during the build

Even a freshly pulled base can lag a distribution's security feed by a few days. Running the package manager's upgrade command during the build closes that remaining gap:

# Debian / Ubuntu bases
FROM debian:12-slim
RUN apt-get update \
    && apt-get upgrade -y \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Alpine bases
FROM alpine:3.20
RUN apk upgrade --no-cache

# Red Hat / UBI bases
FROM registry.access.redhat.com/ubi9/ubi-minimal
RUN microdnf update -y && microdnf clean all

If your organization requires byte-reproducible builds, this step conflicts with that goal since upgrade pulls whatever is current at build time. In that case, skip this step and rely on Step 2 plus a disciplined rebuild schedule (Step 6) as your patch mechanism instead.

Step 4: Patch packages that have no upstream base-image fix yet

Sometimes the CVE is fixed in a specific package version, but neither Step 2 nor Step 3 picks it up yet — the distribution has not shipped the update to its repositories. In that case, pin the fixed version explicitly:

# Debian/Ubuntu: install an explicit fixed version
RUN apt-get update \
    && apt-get install -y --no-install-recommends openssl=3.0.15-1~deb12u1 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# Alpine: pin to a fixed package version from a specific repo
RUN apk add --no-cache "openssl>=3.3.2-r0"

This is a stopgap, not a long-term strategy — explicit version pins fall out of date and need periodic review. Track them in a comment with the CVE ID and revisit when the base image ships the fix natively, then remove the pin.

Step 5: Pin the base image by digest once it is patched

Once you have a build you trust, pin it by digest so the exact same patched layers ship to every environment until you deliberately update again:

# Find the current digest for a tag
docker pull node:22-slim
docker inspect --format='{{index .RepoDigests 0}}' node:22-slim
# node@sha256:9b2e7a3c... (example)

# Pin the Dockerfile to that digest
FROM node@sha256:9b2e7a3c...

Digest pinning does not mean freezing forever — it means the update is now a visible, reviewable change in your Dockerfile (a one-line diff) rather than a silent change that happens the next time someone runs a build.

Step 6: Put the rebuild-and-patch cycle on a schedule

Manual patching works once. A schedule is what keeps the count down over time. A minimal CI job that rebuilds, scans, and fails on new criticals:

name: base-image-patch-check
on:
  schedule:
    - cron: "0 6 * * 1" # every Monday
  workflow_dispatch: {}
jobs:
  rebuild-and-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build --pull --no-cache -t myapp:ci .
      - run: docker save myapp:ci -o myapp.tar
      - run: curl -fsSL https://scanrook.io/install.sh | sh
      - run: scanrook scan --file myapp.tar --format json --out report.json
      - run: |
          CRIT=$(jq '.summary.critical' report.json)
          if [ "$CRIT" -gt 0 ]; then
            echo "::error::$CRIT critical findings after patch rebuild"
            exit 1
          fi

Our GitHub Actions scanning guide covers the full pipeline, including uploading the report as a build artifact and commenting results on pull requests.

Verifying the patch worked

Scan before and after every patch round and diff the severity totals with the same scanner version, so the comparison is apples to apples:

docker save myapp:before -o before.tar
scanrook scan --file before.tar --format json --out before.json

docker save myapp:patched -o after.tar
scanrook scan --file after.tar --format json --out after.json

jq '.summary' before.json after.json

If a finding survives the full cycle, it likely has no fix published anywhere yet. At that point the question shifts from patching to triage: is the affected package actually reachable at runtime, and does switching to a smaller base image remove the package entirely instead of waiting on a patch. Our broader guide to reducing CVEs in Docker images covers that path in detail.

Where ScanRook fits

Patch cadence only works if you can measure it. ScanRook scans a saved image tar locally or in CI, matches installed packages against OSV, NVD, and vendor advisory data, and returns severity totals you can gate a build on directly — so the weekly rebuild job above is enforceable, not aspirational. See the docs for CI recipes across common pipelines.

Frequently asked questions

How do I patch vulnerabilities in a Docker base image?

Rebuild with --pull --no-cache, run the package manager's upgrade command during the build, then rescan to confirm the findings dropped.

Does docker pull get the patched image automatically?

Only if you pull the same tag again with --pull. Your local cache otherwise keeps serving the old, unpatched layer.

Should I pin my base image to a digest or a tag?

Pin to a digest for reproducible builds, then update the digest deliberately on a schedule — treat the update as a reviewable one-line diff.

How often should base images be patched?

Weekly at minimum, nightly for internet-facing services. Automate the cycle in CI so it does not depend on someone remembering.

Verify every patch round with ScanRook

Scan before-and-after builds, diff severity totals, and gate CI on the result so your patch cadence is measurable instead of assumed.

Related Posts

More on this topic.