The core idea
An image contains hundreds of packages you didn't write: the OS, the language runtime, your dependencies. Some of them have known vulnerabilities (CVEs). A scanner lists every package in the image and matches it against vulnerability databases. The skill isn't running the scanner; it's reading the result: which findings matter, and which you can fix.
Run a scan
Trivy is a free, open-source scanner that runs as a container, so there's nothing to install:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v trivy-cache:/root/.cache aquasec/trivy:0.76.1 image notes-apiThe socket mount lets Trivy read your local images; the cache volume keeps its vulnerability database between runs.
(Docker Desktop also includes Docker Scout, docker scout cves, which does the same job with a Docker account.)
The numbers
Counts from this course's scans, all with the same Trivy version on the same day:
| Image | Critical | High | Medium | Low | Total |
|---|---|---|---|---|---|
node:22 | 20 | 455 | 2042 | 1789 | 4397 |
notes API on node:22-slim | 1 | 42 | 110 | 84 | 239 |
node:22-alpine | 0 | 10 | 12 | 1 | 23 |
The full Debian image has nearly twenty times the findings of the slim one, because it ships far more packages: compilers, git, Python and their libraries. Base image choice (02.07 Choosing a base image) is your biggest lever. But 239 still sounds alarming, so filter to what you can act on.
Only what's fixable
Many findings have no fix released yet; you can't do anything about those except choose a different base. Trivy's
--ignore-unfixed hides them:
$ trivy image --scanners vuln --ignore-unfixed notes-api...notes-api fixable: CRITICAL=0 HIGH=10 MEDIUM=12 LOW=1 total 24
(Summary of Trivy's report.) 239 became 24. Then ask where those 24 live:
$ trivy image --scanners vuln --ignore-unfixed --severity HIGH --format json notes-api | ...Node.js {'brace-expansion': 5, 'ip-address': 1, 'pacote': 2, 'picomatch': 1, 'sigstore': 1}
Every fixable HIGH was inside npm's own bundled packages, part of the Node.js installation. The notes API never runs npm in production. So the final stage now deletes it in the same step that uses it:
# Install production dependencies, then delete npm itself: the app never runs it, and its bundled
# packages were the only fixable HIGH findings in a scan (see 07.03). Same RUN, so they never reach a layer.
RUN npm ci --omit=dev \
&& rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx$ trivy image --scanners vuln --ignore-unfixed notes-api...notes-api fixable: CRITICAL=0 HIGH=0 MEDIUM=0 LOW=0
Zero fixable findings. The image isn't smaller, because npm also lives in a lower layer from the base image (02.05 Making images small), but scanners and attackers both see the final filesystem, where it's gone.
Smaller isn't automatically safer
You might expect distroless (02.07 Choosing a base image) to win. Built the same day, it didn't:
notes-api:distroless --ignore-unfixed: CRITICAL=1 HIGH=6 MEDIUM=12 LOW=14notes-api:distroless (debian 12.13) libssl3 CRITICAL 3.0.18-1~deb12u2 -> fixed in 3.0.19-1~deb12u2
Its OpenSSL was a patch release behind the slim image's. Fewer packages is good, but only if they're up to date.
Measure; don't assume. And rebuild regularly with docker build --pull so you pick up base-image patches.
Gating CI
A scan nobody reads does nothing, so make the build fail on what matters. --exit-code 1 makes Trivy fail when it
finds anything at the chosen severities:
$ trivy image --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 notes-api$ echo $?0
Before the npm change, the same command exited with 1. "Fixable HIGH or CRITICAL" is a gate teams can live with: it fails on things you can fix today, not on noise. The workflow in 07.06 Project: ship with GitHub Actions runs exactly this.
A scan shows 200 vulnerabilities in your image, 180 with no fix available. What's the most useful next step?
Scan node:22, node:22-slim and your notes API. Re-run with --ignore-unfixed --severity HIGH,CRITICAL and find which packages the findings come from. Run the gate command and check its exit code.
STUCK? ▸
Different numbers from the table are expected: the vulnerability database changes daily and base images are re-published. Compare images against each other on the same day, not against these numbers.
Recap
Interview questions
How would you introduce image vulnerability scanning into a team's pipeline?