Best practices

How to Scan a Docker Image for Vulnerabilities (4 Ways)

Published June 3, 2026 · 12 min read

Every Docker image you build inherits hundreds — sometimes thousands — of packages from its base image and dependencies. Many of them ship with known vulnerabilities. This guide shows you four practical ways to scan a Docker image for vulnerabilities, how to read what the scanner reports, and how to actually fix the findings instead of drowning in a wall of CVEs.

What “scanning a Docker image” actually means

A Docker image is a stack of filesystem layers. A vulnerability scanner unpacks those layers and builds an inventory of everything installed, then checks each item against one or more vulnerability databases. There are two distinct categories it inventories:

  • OS packages — read from the package manager's own database: /lib/apk/db/installed (Alpine), /var/lib/dpkg/status (Debian/Ubuntu), or the RPM database (RHEL/Fedora).
  • Application dependencies— language ecosystems like npm (package-lock.json), pip, Go modules, Ruby gems, and Java JARs found inside the image.

The scanner then matches each package and version against vulnerability data and emits a list of findings: a CVE identifier, the affected package, a severity, and — ideally — the version that fixes it. If you want the deeper background on where that CVE data comes from, see our comparison of NVD, OSV, GHSA, and Snyk Intel.

Method 1 — Docker Scout (built in)

If you already have Docker Desktop or a recent Docker CLI, you have a scanner installed. Docker Scout reads your local image and reports CVEs with no extra tooling:

# Quick overview of an image
docker scout quickview my-image:tag

# Full CVE list, filtered to High and Critical
docker scout cves my-image:tag --only-severity critical,high

Scout is the lowest-friction option for a quick local check. Its weakness is depth: it is strongest on OS packages and the most common ecosystems, and it is tied to the Docker tooling. For a thorough audit you will usually reach for one of the dedicated scanners below.

Method 2 — Trivy

Trivy is the most widely used open-source image scanner. Install it, then point it at a local image, a remote image, or a saved tarball:

# Scan a local or remote image by name
trivy image my-image:tag

# Scan an exported tarball (no daemon needed)
trivy image --input my-image.tar

# Fail a CI build on High/Critical findings
trivy image --exit-code 1 --severity HIGH,CRITICAL my-image:tag

Trivy is fast and covers OS packages plus most language ecosystems. The --exit-code 1 flag is the one to remember — it turns the scan into a build gate.

Method 3 — Grype

Grype, from Anchore, is the other popular open-source CLI. It pairs naturally with Syft (an SBOM generator from the same project) and reads the same image sources:

# Scan an image by name
grype my-image:tag

# Scan a saved tarball explicitly
grype docker-archive:my-image.tar

# Only fail on a severity threshold
grype my-image:tag --fail-on high

Grype and Trivy will often report slightly different counts on the same image. That is expected — they use different databases and matching rules. We dig into why in the 2026 scanner benchmark.

Method 4 — ScanRook (no install required)

If you do not want to install anything — or you want a single report that merges OSV, NVD, and vendor advisory data with EPSS exploit-probability scores — you can scan a Docker image with ScanRook in three ways:

  • Web upload: export the image to a tar (see below) and drag it onto the ScanRook dashboard. No CLI, no agent.
  • Registry scan: connect a registry (Docker Hub, GHCR, ECR, or any OCI-compliant registry) and scan an image by name and tag without pulling it yourself.
  • CLI: install the scanner and run it locally:
    curl -fsSL https://scanrook.sh/install | bash
    scanrook scan my-image.tar

ScanRook also unpacks nested archives and shaded JARs that simpler file-path scanners miss, and it reads the actual installed package state rather than guessing from advisory metadata. That difference is the subject of our deep dive on installed-state scanning vs. advisory matching.

How to export an image to a tarball

Three of the four methods above can read a tarball, which is the most portable way to move an image to a scanner. Export any local image with docker save:

# Build, then export to a tarball
docker build -t my-image:tag .
docker save my-image:tag -o my-image.tar

The resulting my-image.tar contains every layer and the image manifest. This is exactly what you upload to ScanRook or feed to Trivy and Grype on a machine that does not have the original image loaded.

Sanity checklist: is the scan even valid?

Before you read a single finding, confirm the scanner actually saw the image you think it saw. Most “the scanner is wrong” complaints turn out to be a scan of the wrong artifact, the wrong architecture, or a tarball the scanner could not fully parse. Run through this list once per pipeline — not once per scan — and it stops being an issue.

Before you scan

  • Use docker save, not docker export. docker export flattens a running container to a filesystem tar and throws away the image manifest and layer metadata, so a scanner reading it sees far less than it should.
  • Match the architecture you actually deploy. On a multi-arch tag, docker save exports whatever platform you pulled. If you ship arm64 and scan an amd64 pull, the package versions can differ. Pull explicitly with --platform.
  • Export the final stage of a multi-stage build, not an intermediate one. A tarball produced from a --target build stage will contain the compiler toolchain and none of the runtime layers.
  • Resolve the tag to a digest and record it. Tags move. If you scan my-image:latest on Monday and deploy my-image:latest on Friday, you have audited a different image.
  • Refresh the vulnerability database before a gating run. A scanner with a stale local database quietly reports fewer findings, and a clean report is indistinguishable from a broken one.
  • Authenticate to the registry before pulling a private image, and check the pull actually succeeded. A silently failed pull that falls back to a stale local copy is the classic way to scan last month's image.

After the scan, before you trust it

  • Check the package count, not just the finding count. If the scanner reports zero OS packages on a Debian- or Alpine-based image, it failed to parse the package database — that is a broken scan, not a clean image.
  • Confirm the ecosystems you expect are represented. A Node app whose report lists only OS packages means the npm dependency tree was never walked.
  • Treat a suspiciously empty result as a failure. Wire your CI to fail when the report contains no packages at all, the same way you fail on a policy violation.
  • Check whether findings are backed by the package manager database or by filename heuristics before you escalate any of them.
  • Re-scan the same tarball with the database refreshed if a report looks unexpectedly different from the previous build. Confirm the change came from your image, not from the data source.

Validity checklist for container image scans. Structural guidance only — no thresholds are prescribed, because the right ones depend on your base image and release cadence.

How to read the results

Every scanner reports the same core fields. Learn to read them and the output stops being a wall of red:

FieldWhat it tells you
CVE IDThe unique identifier for the vulnerability. Look it up to understand impact.
Package & versionThe exact installed component that is affected.
Severity / CVSSTheoretical impact. Useful, but not the same as real-world risk.
Fixed versionThe version that resolves it. “won't fix” or blank means no patch yet.
EPSS / KEVExploit probability and known-exploited status — the best signal for what to fix first.

Do not sort by severity alone. Roughly half of all CVEs are rated High or Critical, so severity is a weak filter. Sort by exploit probability instead — see how to prioritize vulnerabilities with EPSS.

How to fix the vulnerabilities you find

Scanning is only half the job. Most findings fall into one of three buckets, each with a standard fix:

  • OS package CVEs: rebuild on an updated base image (e.g. bump FROM debian:12.4 to the latest patch tag) or add an apt-get upgrade step. A stale base image is the single biggest source of findings.
  • Application dependency CVEs:bump the dependency in your lockfile and rebuild. The scanner's “fixed version” column tells you the target.
  • Smaller attack surface:switch to a minimal base image. A distroless or slim image simply contains fewer packages, so it has fewer CVEs to begin with — see Alpine vs Debian vs Distroless.

Automate it in CI/CD

A scan you run by hand once a month is not a control. Wire scanning into your pipeline so every image is checked before it ships, and fail the build on a threshold you choose. Here is the idea in a GitHub Actions step using Trivy:

- name: Scan image
  run: |
    docker build -t my-image:${{ github.sha }} .
    trivy image --exit-code 1 --severity HIGH,CRITICAL my-image:${{ github.sha }}

ScanRook can do the same with a policy that gates deployments on severity, EPSS, or CISA KEV membership rather than a single severity threshold. For the broader playbook, read container scanning best practices.

Common pitfalls

  • Scanning only the base, not the final image. Your application layers add dependencies. Always scan the final built artifact.
  • Ignoring nested JARs.Java apps often bundle dependencies inside shaded or uber-JARs. A scanner that does not unpack them will miss real CVEs — this is exactly how Log4Shell hid in so many images.
  • Treating every Critical as urgent. A Critical CVE in a package that is never reachable at runtime can wait; a High CVE with a public exploit cannot. Prioritize by exploitability.
  • Scanning once. New CVEs are published daily. Re-scan deployed images on a schedule, not just at build time.

Frequently asked questions

Can I scan a Docker image without installing anything?

Yes. Export the image with docker save my-image:tag -o image.tar and upload the tar to the ScanRook dashboard, or connect a registry and scan by name. No CLI or agent is required.

Trivy, Grype, and Scout give different counts — which is right?

None is simply “right.” They use different vulnerability databases and matching logic. Higher counts often mean more false positives from loose advisory matching, not better coverage. What matters is whether a finding reflects a package actually installed and reachable.

Should the scan fail my build?

Yes, on a threshold you choose. Failing on High and Critical is a common starting point; maturing teams gate on exploit probability (EPSS) and CISA KEV membership instead, which blocks far fewer builds while catching the findings that matter.

How is scanning an image different from generating an SBOM?

An SBOM is the inventory of what is in the image; a vulnerability scan is the inventory matched against vulnerability data. Most scanners generate an SBOM internally as the first step. See what an SBOM is for more.

Scan a Docker image in seconds with ScanRook

Export your image with docker save and upload the tar, or connect a registry to scan by name. ScanRook merges OSV, NVD, and vendor advisory data with EPSS exploit scores into one prioritized report — no agent to install.

Related Posts

More on this topic.