July 12, 2026 · Docker HQ Team

Docker Image Vulnerability Scanning: A Practical Guide

Docker image vulnerability scanning is the process of inspecting an image's OS packages and application dependencies against known CVE databases to find security flaws before that image runs in production. You scan an image by pointing a tool like Trivy, Grype, or docker scout at it, then triaging the results by severity and whether a fix exists. Do it in CI on every build, and re-scan images already running, because new CVEs get published against layers you shipped months ago.

Why Docker image vulnerability scanning matters

A Docker image is a stack of layers. The base layer alone (debian:bookworm, node:20, whatever you pulled) drags in hundreds of OS packages you never chose. On top of that sit your application dependencies: npm modules, Python wheels, Go binaries, Java jars. Every one of those is a place a CVE can hide.

The scary part is that an image can go from clean to vulnerable without you touching it. You build myapp:1.4 today, it scans clean, you deploy it. Three weeks later someone publishes a CVE against a version of libssl baked into that image. The image is now vulnerable and nothing changed on your end. That is why scanning is a repeated job, not a one-time gate.

Scanning does not fix anything by itself. It gives you a list. What you do with the list is where the actual security work happens.

What scanners actually check

Two categories, and it helps to know which your tool covers:

  • OS packages — things installed by apt, apk, yum, dnf. The scanner reads the package database inside the image and matches versions against advisories from Debian, Alpine, Red Hat, and others.
  • Application dependencies — parsed from lockfiles and manifests: package-lock.json, go.sum, requirements.txt, Gemfile.lock, pom.xml. Matched against sources like the GitHub Advisory Database and OSV.

Most modern scanners do both. Some also flag misconfigurations (running as root, no HEALTHCHECK), leaked secrets, and can generate an SBOM. Don't assume every tool checks every layer. Read what your scanner supports.

Scanning with Trivy

Trivy from Aqua Security is the one I reach for first. It is a single binary, no daemon, and it scans OS packages and language dependencies out of the box.

# Scan a local or remote image
trivy image myapp:1.4

# Only show HIGH and CRITICAL, and only issues that have a fix available
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:1.4

--ignore-unfixed is the flag that keeps you sane. A lot of the CVEs a scanner reports have no patched version available yet, so there is nothing you can do about them today. Filtering them out shows you the work you can actually act on.

For CI, make the scan fail the build when it finds something real:

trivy image --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed myapp:1.4

Exit code 1 breaks the pipeline. That is the whole point of a gate.

Scanning with docker scout

If you are already in the Docker ecosystem, docker scout ships with recent Docker Desktop and the CLI. No extra install.

# Quick overview grouped by severity
docker scout quickview myapp:1.4

# Full CVE list
docker scout cves myapp:1.4

# Compare a new tag against a previous one to catch regressions
docker scout compare --to myapp:1.3 myapp:1.4

The compare command is genuinely useful before a release. It tells you whether the new image adds vulnerabilities the old one didn't have, which is a cleaner signal than an absolute count.

Grype for a second opinion

No scanner has a perfect database. Grype (from Anchore) pulls from different advisory sources than Trivy, so running both occasionally catches things one misses.

grype myapp:1.4 --only-fixed

You do not need three scanners in CI. Pick one as your gate. Keep a second one around for when a result looks wrong and you want to confirm it. If you are choosing between them, we wrote a longer breakdown in Docker Security Scanning Tools Compared (2026).

Put it in CI, not in someone's memory

Manual scans get skipped the week everyone is busy, which is exactly the week a bad image ships. Wire it into the pipeline so it runs on every push. Here is the shape of it in GitHub Actions:

- name: Build image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    severity: HIGH,CRITICAL
    ignore-unfixed: true
    exit-code: 1

Now a build with an unpatched HIGH CVE cannot merge without someone looking at it. That is the gate doing its job.

Fixing what the scan finds

The list is triaged, now what. In rough order of leverage:

Update the base image. Most CVEs live in the base, not your code. Rebuild against the current patch release of your base tag and a big chunk of findings disappear. Pin to a digest so builds are reproducible, and bump that digest on a schedule.

FROM node:20.18.1-bookworm-slim

Switch to a slimmer base. Fewer packages means a smaller attack surface and fewer CVEs to chase. The -slim and -alpine variants strip out a lot. Distroless images (gcr.io/distroless/*) go further and ship almost no OS packages at all, which leaves the scanner very little to flag.

Bump the vulnerable dependency. If the CVE is in a language package, update it in your lockfile and rebuild.

Don't run as root. This does not remove a CVE, but it limits what an attacker can do if they exploit one. A USER line is one of the cheapest hardening steps you have. See How to Run a Docker Container as a Non-Root User for the full setup.

For the rest of the checklist beyond scanning, our guide on Docker Security Best Practices for Production covers it.

Re-scan what's already running

This is the step almost everyone forgets. Your CI gate protects new builds. It does nothing for the image that passed its scan two months ago and has been running ever since. New CVEs are published daily, and some of them will land on layers you already deployed.

So schedule a re-scan of your running images. A nightly cron that runs trivy image against every tag in your registry and reports new HIGH and CRITICAL findings is enough to start.

The catch is knowing about it at 3am when it matters, not at your next standup. This is where managing hosts from your phone helps. Docker HQ can scan the images on your remote servers over SSH and send a push alert when a new critical vulnerability shows up on something you're actually running, so you hear about it from a notification instead of from an incident. See pricing for what's included.

Frequently asked questions

How often should I scan Docker images?

On every build in CI, and on a recurring schedule (nightly or weekly) for images already deployed. The build scan catches problems before shipping. The scheduled scan catches CVEs published after you shipped, which is the larger long-term risk.

What's the difference between HIGH and CRITICAL severity?

Severity comes from the CVSS score attached to each CVE. CRITICAL (9.0-10.0) usually means remote code execution or full compromise with little effort. HIGH (7.0-8.9) is serious but often needs specific conditions to exploit. Gate on both, and treat CRITICAL as drop-everything.

Should I block a build if the CVE has no fix?

Usually no. If there is no patched version, blocking the build just stops you from shipping with no way to unblock it. Use --ignore-unfixed in your gate and track unfixable CVEs separately, then act when a patch lands. A CVE in a package your app never calls at runtime is also a candidate to accept and move on.

Is scanning enough to secure my images?

No. Scanning finds known CVEs in packages. It does not catch logic bugs, bad secrets management, over-permissive containers, or exposed ports. Treat it as one layer. Combine it with a minimal base image, a non-root user, dropped capabilities, and network controls.

Start with one command today: run trivy image against the image you have in production right now. Whatever it prints is your actual baseline, and it is usually worse than people expect.