Trivy container security scanning has become the de-facto standard for detecting vulnerabilities in Docker images before they reach production. If you have ever wondered how attackers exploit known CVEs in base images like ubuntu:20.04 or node:18, or whether your Dockerfile exposes secrets, Trivy gives you the answer in seconds. This guide covers installation, scanning strategies, CI/CD integration, and how to interpret and act on Trivy findings — without drowning in false positives.
Prerequisites
- Docker installed and running (Docker 20.10+)
- Linux, macOS, or Windows (WSL2) workstation
- Basic familiarity with Docker images and Dockerfiles
- A CI/CD platform (GitHub Actions, GitLab CI, or Jenkins) for pipeline integration
- Root or sudo access for system-wide Trivy installation (or use the binary install)
Installing Trivy
Trivy ships as a single static binary with no external dependencies. The quickest path on Linux:
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
trivy --version
On Ubuntu/Debian you can add the Aqua Security apt repository for managed updates:
sudo apt-get install wget apt-transport-https gnupg lsb-release -y
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy -y
On macOS with Homebrew: brew install trivy
Trivy downloads its vulnerability database on first run. Force a database update at any time with trivy image --download-db-only.
Scanning Docker Images for Vulnerabilities
The core command is straightforward:
trivy image nginx:latest
Trivy pulls the image if not cached locally, unpacks the layers, and checks every installed OS package and language-level dependency against its vulnerability database (NVD, GitHub Advisory, Alpine secdb, and others). Output shows CVE IDs, severity, installed version, and the fixed version when available.
Filtering by Severity
By default Trivy shows all severities from UNKNOWN to CRITICAL. In practice you want to focus on actionable findings:
trivy image --severity HIGH,CRITICAL nginx:latest
Ignoring Unfixed Vulnerabilities
Many CVEs have no available fix yet. Use --ignore-unfixed to skip them and focus on what you can actually remediate:
trivy image --severity HIGH,CRITICAL --ignore-unfixed nginx:latest
Output Formats
Trivy supports multiple output formats for different consumers:
# Human-readable table (default)
trivy image nginx:latest
# JSON for programmatic processing
trivy image --format json --output results.json nginx:latest
# SARIF for GitHub Security tab integration
trivy image --format sarif --output results.sarif nginx:latest
# CycloneDX SBOM
trivy image --format cyclonedx --output sbom.json nginx:latest
The SARIF format integrates directly with GitHub’s Security tab, showing vulnerability annotations inline on pull requests.
Scanning Dockerfiles and IaC Misconfigurations
Trivy’s config scanner checks Dockerfiles, Kubernetes manifests, Terraform, and Helm charts for security misconfigurations before they are deployed:
# Scan a Dockerfile
trivy config ./Dockerfile
# Scan an entire directory (Kubernetes manifests, Terraform)
trivy config ./k8s/
# Scan Helm charts
trivy config --helm-values values.yaml ./charts/myapp
Common Dockerfile findings include: running as root, using ADD instead of COPY, missing HEALTHCHECK, exposing sensitive ports, and not pinning base image digests.
Detecting Secrets and Sensitive Data
Trivy can scan image layers and filesystems for accidentally baked-in secrets:
# Enable secret scanning on an image
trivy image --scanners secret nginx:latest
# Scan a local directory for secrets
trivy fs --scanners secret ./src/
Trivy detects AWS keys, GitHub tokens, private SSH keys, database connection strings, and hundreds of other secret patterns. This is particularly valuable for catching secrets committed to source code that ended up in a Docker layer.
Comparing Trivy Against Alternatives
| Feature | Trivy | Grype | Snyk | Clair |
|---|---|---|---|---|
| OS package CVEs | Yes | Yes | Yes | Yes |
| Language deps | Yes | Yes | Yes | No |
| IaC misconfigs | Yes | No | Yes | No |
| Secret scanning | Yes | No | Yes | No |
| SBOM generation | Yes | Yes | Yes | No |
| License | Apache 2.0 | Apache 2.0 | Proprietary | Apache 2.0 |
| CI/CD integration | Native | Native | Native | Limited |
| Offline mode | Yes | Yes | No | Partial |
Trivy’s advantage is breadth — it combines vulnerability scanning, misconfiguration detection, secret scanning, and SBOM generation in a single binary without requiring a running server or database.
Integrating Trivy into CI/CD Pipelines
GitHub Actions
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker 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: HIGH,CRITICAL
exit-code: 1
ignore-unfixed: true
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
The exit-code: 1 setting fails the pipeline when critical or high CVEs are found. if: always() on the SARIF upload ensures results are visible even when the scan step fails.
GitLab CI
trivy-scan:
image: aquasec/trivy:latest
stage: test
script:
- trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false
Real-World Scenario
You have a production application running node:16 as its base image. Your security team flags that the CI/CD pipeline never validates container images before deployment. You have 48 hours to implement a scanning gate.
Step 1 — Audit the current image:
trivy image node:16 --severity HIGH,CRITICAL --ignore-unfixed
This reveals 47 HIGH and 12 CRITICAL CVEs in the base image — most fixed by upgrading to node:18-alpine.
Step 2 — Update the Dockerfile:
# Before
FROM node:16
# After
FROM node:18-alpine
Step 3 — Re-scan to confirm remediation:
trivy image node:18-alpine --severity HIGH,CRITICAL --ignore-unfixed
The alpine variant drops the CVE count from 59 to 3 (all unfixed).
Step 4 — Add the CI gate:
Add the GitHub Actions workflow above. All future PRs are blocked if CRITICAL CVEs are introduced.
Step 5 — Scan IaC too:
trivy config ./k8s/ --severity HIGH,CRITICAL
This surfaces two issues: a deployment running as root and a missing readOnlyRootFilesystem security context setting.
Gotchas and Edge Cases
Vulnerability database staleness: Trivy caches its database locally. In CI environments run trivy image --download-db-only as a separate cached step to avoid redundant downloads.
Private registry authentication: Export credentials as environment variables before scanning:
export TRIVY_USERNAME=myuser
export TRIVY_PASSWORD=mypassword
trivy image myregistry.example.com/myapp:latest
False positives: Some CVEs are flagged in libraries that your application never calls. Use .trivyignore to suppress known false positives:
# .trivyignore
CVE-2022-12345 # Not exploitable — library not called at runtime
Alpine musl vs glibc: Alpine-based images report fewer CVEs because Alpine uses musl libc and the Alpine security team patches aggressively. This is not “missing” coverage — it reflects genuine differences in the library ecosystem.
Multi-arch images: Always specify the platform when scanning multi-arch images to get the right results: trivy image --platform linux/amd64 myapp:latest.
Trivy vs runtime security: Trivy finds vulnerabilities at build time. It does not replace runtime security tools like Falco that detect unexpected behavior in running containers.
Troubleshooting
“No such image” error: Trivy cannot find the image locally and cannot pull it. Confirm the image name, tag, and registry credentials.
Database download failures in air-gapped environments: Download the database bundle on an internet-connected machine with trivy image --download-db-only, then transfer the ~/.cache/trivy/ directory to the air-gapped host.
High memory usage on large images: Use --parallel 1 to reduce concurrent layer processing: trivy image --parallel 1 myapp:latest.
Exit code 1 but no critical CVEs shown: Check whether --ignore-unfixed is omitted — unfixed critical CVEs can trigger the exit code even if you expected to see only fixable issues.
Scan timeout in CI: Set TRIVY_TIMEOUT=10m to extend the default 5-minute timeout for large images.
Summary
- Trivy is a single-binary scanner covering CVEs, misconfigurations, secrets, and SBOM generation
- Run
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:latestas your baseline scan - Use
--exit-code 1in CI/CD to block deployments with critical vulnerabilities - Combine
trivy image(runtime) withtrivy config(Dockerfile/IaC) for full coverage - Alpine-based images dramatically reduce CVE surface area
- Use
.trivyignoreto suppress confirmed false positives and reduce alert fatigue - Upload SARIF results to GitHub Security tab for inline PR annotations
- Trivy does not replace runtime security — pair it with Falco for defense in depth