GICT knowledge base
How to Scan Docker Images for Vulnerabilities with Trivy
Published
A container image can start normally while carrying packages with known vulnerabilities. Trivy reads the image contents and compares detected package versions with vulnerability advisories. You do not need to start the application to inspect it.
This guide installs Trivy on Ubuntu, scans a public image, saves a report, and checks how severity filters affect the command’s exit status. The commands were verified on a disposable Ubuntu 24.04.4 CPU VM, with the image digest and database metadata recorded. Vulnerability counts are dated observations, not permanent properties of an image tag.
Prepare a scanning workspace
Use Ubuntu 24.04 on an x86-64 machine, a sudo-capable account, and outbound HTTPS access to GitHub, image registries, and Trivy’s database registries. A local Linux machine is sufficient. If you need a separate scanning host and do not already have a VPS, an interServer VPS is an option. A GPU is not required. Leave room for the vulnerability database and downloaded image layers; larger images need more resources than this small example.
Docker Engine is not required for this workflow. The --image-src remote option reads the image from its registry instead of consulting a local Docker daemon. It downloads and analyzes content but does not run the image.
All commands below run in the same Bash terminal. The $ character is the prompt; do not copy it.
$ uname -m
$ sudo apt-get update
$ sudo apt-get install -y curl ca-certificates jq
$ mkdir -p ~/trivy-lab/bin ~/trivy-lab/downloads ~/trivy-lab/reports
$ cd ~/trivy-lab/downloads
Check that the architecture is x86_64. Use the corresponding release archive on other architectures rather than installing this binary unchanged.
Install a pinned Trivy release
Download Trivy 0.74.0 and its published checksums from the official release:
$ curl -fLO https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_Linux-64bit.tar.gz
$ curl -fLO https://github.com/aquasecurity/trivy/releases/download/v0.74.0/trivy_0.74.0_checksums.txt
$ grep ' trivy_0.74.0_Linux-64bit.tar.gz$' trivy_0.74.0_checksums.txt | sha256sum -c -
$ tar -xzf trivy_0.74.0_Linux-64bit.tar.gz -C ../bin trivy
$ cd ~/trivy-lab
$ ./bin/trivy --version
Expected checksum result:
trivy_0.74.0_Linux-64bit.tar.gz: OK
Stop if checksum verification fails. The checksum detects a mismatched download; it is not independent proof of publisher identity when both files come from the same source. The binary stays in this workspace, leaving any existing system installation unchanged.
Scan an image and save its identity
Use this older Alpine release only as a scan example, not as a deployment recommendation. For your own work, replace it with the image you intend to deploy.
$ umask 077
$ IMAGE='docker.io/library/alpine:3.21.0'
$ ./bin/trivy image --image-src remote --platform linux/amd64 --scanners vuln --format json --output reports/full.json "$IMAGE"
$ echo $?
$ ./bin/trivy --version
$ jq '.Metadata | {OS, RepoDigests}' reports/full.json
The first scan downloads the vulnerability database and image data. Let it finish; a download or authentication error is not a vulnerability result. --scanners vuln makes the scope explicit and leaves secrets and configuration scanning out of this guide.
The default exit status is zero even when vulnerabilities are found. That means this command completed, not that the image passed a security policy. Check the log for detected operating-system and package information, then inspect the JSON report.
Capture an immutable reference for later comparisons:
$ PINNED_IMAGE=$(jq -er '.Metadata.RepoDigests[0]' reports/full.json)
$ echo "$PINNED_IMAGE"
A tag can move to different content. The digest and explicit platform identify the content selected for these scans. Save the version output too: its database timestamps explain why a later scan of the same digest may return different findings. Reports can reveal internal image names and software versions, so keep them private.
Read package findings before choosing a fix
List the package, vulnerability, severity, installed version, and fixed version:
$ jq -r '.Results[]? | .Vulnerabilities[]? | [.PkgName, .VulnerabilityID, .Severity, .InstalledVersion, (.FixedVersion // "")] | @tsv' reports/full.json
One line from the test report was:
musl CVE-2025-26519 HIGH 1.2.5-r8 1.2.5-r9
The installed version is what Trivy found in the image. A fixed version is an advisory’s repair target for that package and distribution; it is not an instruction to install an unrelated upstream release. A blank fixed-version field means Trivy does not provide a fix version for that finding. It does not mean the finding is harmless.
Read the linked advisory and consider whether the affected component is used or exposed in your application. Distribution vendors may backport patches without adopting the newest upstream version. Trivy’s severity can therefore differ from a generic CVE website.
For a base-image issue, choose a maintained replacement, rebuild the application image, and scan the rebuilt image. Updating packages on the VPS does not repair packages already baked into an image. Running Trivy only reports findings; it does not patch them. Application testing is still needed before deploying a rebuilt image.
Apply a severity policy without hiding the full report
Keep full.json as your unfiltered reference. Write a separate report containing high and critical findings, and use exit code 10 for policy failure:
$ ./bin/trivy image --image-src remote --platform linux/amd64 --scanners vuln --severity HIGH,CRITICAL --exit-code 10 --format json --output reports/high-critical.json "$PINNED_IMAGE"
$ echo $?
The test returned:
10
On September 10, 2026, the full report contained 56 package-vulnerability entries: 21 low, 14 medium, 19 high, and two critical. The filtered report retained 21 high/critical entries. These are package findings, not 56 distinct CVEs; one CVE can affect several packages. The database was updated at 07:06 UTC that day.
Here, 10 means matching findings were detected. 0 means none matched this filter in a completed scan. Another nonzero result means the command did not satisfy this policy check and needs investigation; do not relabel it as a clean scan. Check the error log and whether a fresh report was written. In a script using set -e, handle the expected policy exit explicitly instead of assuming commands after it will run.
For a separate repair queue, add --ignore-unfixed:
$ ./bin/trivy image --image-src remote --platform linux/amd64 --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --format json --output reports/fixable.json "$PINNED_IMAGE"
All 21 high/critical entries in this test had a fixed version, so the repair-queue filter retained all 21. This narrows the displayed findings to those with known fixes. It does not reduce the image’s vulnerabilities. Do not substitute this report for the full report when deciding whether deployment is acceptable. Likewise, filtering out lower severities is a prioritization choice, not a repair.
Avoid misleading results
- Do not count stale reports. Use a new output path for a new assessment, especially in automation. In the test, a nonexistent image tag returned exit 1 and left the previous JSON file unchanged. A failed registry request must not leave you interpreting an older file as today’s result.
- Record the database date. An unchanged image can gain new findings when advisories arrive. Keep updates enabled for ordinary scans; do not freeze an old database to preserve a favorable count.
- Check the target and platform. A similarly named image, a changed tag, or another architecture can have different packages. Compare digests as well as labels.
- Look for unsupported or end-of-life software. No listed CVEs is not equivalent to ongoing vendor support. Review Trivy’s warnings and the distribution lifecycle.
If a scan cannot download data, check outbound HTTPS access, registry authentication, rate limits, and the exact image reference. For private registries, use the supported credential flow without embedding passwords in published commands. Do not disable TLS verification to make a download pass.
What an image scan cannot prove
Trivy is useful before deployment and during periodic reassessment, but it does not establish that an application is secure. This vulnerability-only scan does not test application authorization, runtime configuration, reachable attack paths, or secrets. An empty report can also reflect unsupported package types or incomplete analysis.
A cached repeat of the older image scan took 3.69 seconds on the two-vCPU VM. A separate scan of the newer Alpine 3.21 image returned zero findings against the same database, with a different digest recorded. Neither image was run, and this comparison is not an application-compatibility test.
Use the saved digest, database metadata, full report, and filtered policy result together. They give you a reviewable record of what was scanned and why it passed or failed your chosen threshold, without starting a vulnerable application.
Related guides
How to Encrypt and Decrypt Files on Linux with age
Encrypt files with age on Ubuntu, protect your private identity, restore to a separate file, and avoid trusting incomplete decrypted output.
Cyber SecurityA Complete Guide to the Zero Trust Security Model
Discover the Zero Trust security model and its importance in modern cybersecurity, including principles, implementation strategies, and common misconceptions.
