Container images ship with more than your application code — base OS packages, language dependencies, and transitive libraries all come along for the ride, and any one of them can carry a known vulnerability. A single unpatched CVE in a base image can undo weeks of secure coding practice once that image reaches production. This guide walks through how to scan container images with Trivy, the open-source scanner from Aqua Security, so you can catch vulnerable packages, misconfigurations, and exposed secrets before an image is ever deployed. By the end, you'll have Trivy installed locally, know how to run and interpret a scan, understand how to fail builds on critical findings, and have a working pattern for wiring container image scanning into CI/CD.
1. Install Trivy
Trivy runs as a single static binary with no external dependencies, which makes it easy to drop into almost any environment — a laptop, a CI runner, or a container itself.
On macOS with Homebrew:
brew install aquasecurity/trivy/trivy
On Debian/Ubuntu:
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
Or run it straight from a container, no install required:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasecurity/trivy image nginx:latest
Confirm the install with trivy --version. Trivy auto-updates its vulnerability database on first run, pulling from sources like NVD, GitHub Security Advisories, and distro-specific feeds, so a working internet connection is needed the first time you scan.
2. Scan container images with Trivy for the first time
The core command is trivy image, followed by the image name and tag. This is the fastest path to a working trivy vulnerability scanner tutorial — one command gets you a full report.
trivy image nginx:1.25
Trivy pulls the image (or reads it from local Docker/containerd storage if already present), unpacks the layers, and matches installed packages against its vulnerability database. Output looks like this:
nginx:1.25 (debian 12.4)
=========================
Total: 84 (UNKNOWN: 0, LOW: 32, MEDIUM: 41, HIGH: 9, CRITICAL: 2)
┌──────────────┬────────────────┬──────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├──────────────┼────────────────┼──────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────┤
│ libssl3 │ CVE-2024-0727 │ CRITICAL │ fixed │ 3.0.11-1~deb12u2 │ 3.0.13-1 │ OpenSSL: PKCS12 Decoding... │
└──────────────┴────────────────┴──────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────┘
Each row gives you the library, the CVE, severity, whether a fix is available, and the version that resolves it. That "Fixed Version" column is usually your fastest remediation path — bump the base image or package and rescan.
3. Narrow scope with severity filters and scan types
Scanning everything is useful for an initial audit, but in daily workflows you generally want to focus on what's actionable. Filter by severity:
trivy image --severity HIGH,CRITICAL nginx:1.25
Trivy also scans beyond OS packages. Enable additional scanners for a fuller picture:
trivy image --scanners vuln,secret,misconfig nginx:1.25
This adds detection for hardcoded secrets baked into layers (API keys, private keys) and misconfigurations in Dockerfiles or embedded config files, not just CVEs in installed packages.
4. Fail builds automatically with exit codes
A scan that only prints a report doesn't stop a bad image from shipping. Trivy's --exit-code flag turns findings into a build failure:
trivy image --severity CRITICAL --exit-code 1 --ignore-unfixed nginx:1.25
--exit-code 1 makes Trivy return a non-zero status when it finds matches, which any CI system will treat as a failed step. --ignore-unfixed is worth using in gating steps — it suppresses vulnerabilities with no available patch, since failing a build over something you can't yet fix just creates noise and encourages teams to ignore the gate entirely.
For a stricter policy, generate a machine-readable report as well:
trivy image --format json --output results.json --severity HIGH,CRITICAL nginx:1.25
JSON output is what you'll want to feed into a dashboard, a ticketing system, or a policy engine that tracks vulnerability trends over time.
5. Add container image scanning to CI/CD
Manual scans catch problems only if someone remembers to run them. The real value comes from making container image scanning ci/cd a mandatory, automatic gate on every build — no image reaches a registry or a cluster without passing through Trivy first.
Trivy GitHub Actions
Aqua Security publishes an official action, making trivy github actions integration close to copy-paste:
name: Container Scan
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload results to GitHub Security tab
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
Uploading SARIF output surfaces findings directly in the GitHub Security tab, alongside code scanning alerts, so developers see vulnerability data without leaving their normal pull request review flow.
For GitLab CI, the equivalent pattern uses the Trivy container image directly:
container_scan:
stage: test
image:
name: aquasecurity/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity CRITICAL,HIGH $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
6. Keep the vulnerability database current
Trivy's accuracy depends on an up-to-date database. In CI environments, especially ephemeral runners, cache the database between runs to avoid re-downloading it every build and to reduce flakiness from rate limits:
trivy image --cache-dir .trivycache/ nginx:1.25
For air-gapped or high-throughput environments, run Trivy in server mode so scans hit a shared, pre-populated database instead of each job fetching its own copy:
trivy server --listen 0.0.0.0:4954 &
trivy client --remote http://localhost:4954 image nginx:1.25
Troubleshooting and verification
Scan hangs or fails to download the database. Trivy needs outbound access to GitHub Container Registry to pull its vulnerability DB. In restricted networks, pre-download the DB with trivy image --download-db-only on a machine with access, then mount that cache directory into your restricted runner.
Results differ between local and CI runs. Version drift in the Trivy binary or database is the usual cause. Pin the Trivy version in your CI image and check trivy version output includes a recent Vulnerability DB timestamp — a stale DB will under-report new CVEs.
Too many findings to triage. Start by filtering to --severity CRITICAL,HIGH and --ignore-unfixed, then expand scope once the backlog is under control. Track findings over time using the JSON or SARIF output rather than re-reading terminal output each run.
Verify your pipeline is actually gating. Intentionally scan an image known to contain a critical CVE (an old nginx or alpine tag works well) and confirm the CI job fails with a non-zero exit code and that the pull request is blocked from merging. If it isn't, check that exit-code is set and that the step isn't marked continue-on-error: true.
How Safeguard Helps
Trivy is an excellent point-in-time scanner, but supply chain security requires more than a single scan gate — it requires knowing which images are running where, whether they've drifted from what was scanned, and whether a newly disclosed CVE affects artifacts already deployed. Safeguard extends the workflow described above by continuously correlating scan results across your entire software supply chain: build pipelines, registries, and runtime environments, not just the image you happened to scan today.
Safeguard ingests Trivy output (SARIF or JSON) directly from your CI/CD pipeline, deduplicates findings across repositories, and maps each vulnerability back to the specific build, commit, and deployed workload it affects. When a new CVE is disclosed, Safeguard retroactively flags every previously-scanned image that's impacted, so teams aren't left re-scanning their entire fleet by hand to find out what's exposed. Combined with policy enforcement on severity thresholds and provenance verification for the images themselves, Safeguard turns Trivy's per-build gate into an organization-wide, continuously current view of container risk.