Your container images are full of vulnerabilities and you’re shipping them anyway
Don’t feel bad — everyone is. The question is whether you know about it, and whether knowing actually changes anything. Container image scanners are the answer to the first part. The second part is on you.
Three tools dominate this space: Trivy (Aqua Security), Grype (Anchore), and Docker Scout (Docker Inc.). They all scan container images for CVEs. They all produce a wall of anxiety-inducing output. They all cost nothing to get started. But they are meaningfully different in where they shine, where they stumble, and whether you’ll actually use them six months from now.
Here’s the honest breakdown.
Installation: First impressions matter
Trivy
# macOSbrew install aquasecurity/trivy/trivy
# Linux (script install)curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Debian/Ubuntusudo apt-get install -y wget apt-transport-https gnupg lsb-releasewget -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.listsudo apt-get update && sudo apt-get install trivySingle binary. No daemon. No fussing with config files on day one. It just works — and that’s before you discover it also scans Kubernetes configs, Terraform, Helm charts, and your .env files for leaked secrets.
Grype
# macOSbrew install grype
# Linux (script install)curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Verifygrype versionSame energy as Trivy — single binary, curl-and-go. Grype pairs with Syft for SBOM generation; if you want that flow, install both:
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/binDocker Scout
# If you have Docker Desktop — it's already theredocker scout version
# CLI plugin (Linux servers without Desktop)curl -fsSL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh
# Log in (required for most useful features)docker logindocker scout quickview nginx:latestHere’s the thing about Scout: it’s deeply married to Docker Hub and your Docker account. The free tier gives you local scanning and limited Hub image scanning. Serious policy enforcement and historical tracking live behind a paid org plan. That’s not inherently evil, but it’s the first place where friction shows up.
Installation winner: Trivy and Grype are neck-and-neck. Scout gets a penalty for requiring a login to do anything interesting.
Scanning Speed: Three images, one coffee
Let’s run all three against the same targets: nginx:latest, postgres:15, node:20-alpine.
Trivy
# First run downloads the vulnerability DB (~200MB) — subsequent runs are fasttrivy image nginx:latesttrivy image postgres:15trivy image node:20-alpineFirst run: 30–60 seconds (DB download). After that, under 10 seconds per image. Trivy caches the DB at ~/.cache/trivy and auto-updates it.
Grype
grype nginx:latestgrype postgres:15grype node:20-alpineSimilar story — first run pulls the vulnerability DB (~100MB, smaller than Trivy’s). Subsequent scans: 5–15 seconds depending on image size. Grype’s DB lives at ~/.cache/grype.
Docker Scout
docker scout cves nginx:latestdocker scout cves postgres:15docker scout cves node:20-alpineScout leans on Docker Hub’s analysis rather than a local DB, so you’re hitting the network for every scan unless the image is cached. On a fast connection this is fine. On a flaky home lab connection at 2 AM, it’s annoying.
Speed winner: Trivy and Grype are comparable after first run. Scout’s network dependency makes it less predictable.
CVE Coverage and False Positives
All three pull from roughly the same upstream sources — NVD, GitHub Advisory Database, OS vendor advisories (Debian, Ubuntu, Alpine, RHEL, etc.). The differences are in how they reconcile conflicts and how aggressively they filter false positives.
Trivy is comprehensive to a fault. It’ll surface CVEs that are technically present in a package but have no fix available, no real-world exploit, and no path to remediation. This isn’t wrong — it’s accurate. But it means your first scan of node:20-alpine will show 30+ “UNKNOWN” and “LOW” severity findings that you can’t do anything about today. Trivy’s --ignore-unfixed flag saves your sanity:
trivy image --ignore-unfixed --severity HIGH,CRITICAL node:20-alpineGrype tends to produce slightly fewer false positives because it cross-references Anchore’s curated dataset, which applies more context around exploitability. The tradeoff is it can occasionally miss something Trivy catches. In practice on common images, the difference is noise.
Scout surfaces the “actionable” CVEs front and center, with clear guidance on what base image version would fix them. The signal-to-noise ratio feels better for day-to-day use — Docker has clearly optimized for “developer won’t ignore this because it’s too noisy.” The downside is you sometimes wonder what it’s not showing you.
CVE winner: For completeness, Trivy. For developer sanity, Scout. Grype splits the difference.
Output Formats: Because you’ll pipe this into something
Trivy’s buffet
# Table (default, human-readable)trivy image nginx:latest
# JSON (for scripting/ingestion)trivy image -f json -o results.json nginx:latest
# SARIF (for GitHub Advanced Security / VS Code)trivy image -f sarif -o results.sarif nginx:latest
# SBOM in CycloneDX formattrivy image --format cyclonedx nginx:latest
# SBOM in SPDX formattrivy image --format spdx-json nginx:latestTrivy also outputs to GitHub Security format, JUnit XML, and HTML. It’s embarrassingly feature-complete for output formats.
Grype’s approach
# Table (default)grype nginx:latest
# JSONgrype -o json nginx:latest > results.json
# SARIFgrype -o sarif nginx:latest > results.sarif
# Generate SBOM first with Syft, then scan the SBOMsyft nginx:latest -o cyclonedx-json > sbom.jsongrype sbom:./sbom.jsonThe Syft+Grype combo for SBOM-first scanning is legitimately elegant. You generate the inventory of what’s in the image (Syft), then check that inventory for vulnerabilities (Grype). This separates concerns cleanly and lets you store SBOMs for auditing without re-scanning.
Scout’s output
# Formatted table (default)docker scout cves nginx:latest
# JSONdocker scout cves --format json nginx:latest
# SARIFdocker scout cves --format sarif nginx:latest
# Export SBOMdocker scout sbom nginx:latestScout’s table output is the most readable of the three out of the box — color-coded, grouped by severity, with clear remediation paths. Less output format variety than Trivy, but the defaults are better.
CI Integration: Where you’ll actually run this
GitHub Actions
Trivy has an official action:
- name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: 'your-image:latest' format: 'sarif' output: 'trivy-results.sarif' severity: 'HIGH,CRITICAL' ignore-unfixed: true
- name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif'Grype in GitHub Actions:
- name: Scan image with Grype uses: anchore/scan-action@v3 with: image: 'your-image:latest' fail-build: true severity-cutoff: high output-format: sarifScout in GitHub Actions:
- name: Docker Scout CVE scan uses: docker/scout-action@v1 with: command: cves image: 'your-image:latest' sarif-file: scout-results.sarif summary: true env: DOCKER_SCOUT_HUB_USER: ${{ secrets.DOCKER_HUB_USERNAME }} DOCKER_SCOUT_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_TOKEN }}Scout’s GitHub Action requires Docker Hub credentials in CI. Fine for open source projects with public images, mildly annoying for private infra.
Gitea Actions / Forgejo / Drone
For self-hosted CI runners (Gitea Actions, Forgejo, Woodpecker, Drone), Trivy and Grype are the clear winners. Both run as single binaries with no external service dependency — install once in a runner image or pull them on-demand:
jobs: scan: runs-on: ubuntu-latest steps: - name: Install Trivy run: | curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ | sh -s -- -b /usr/local/bin
- name: Scan image run: | trivy image \ --exit-code 1 \ --severity HIGH,CRITICAL \ --ignore-unfixed \ your-registry/your-image:${{ gitea.sha }}Scout in self-hosted CI is a headache — you need Docker Hub creds, network access to Scout’s API, and you’re now dependent on Docker’s availability for your build pipeline. Hard pass for air-gapped or privacy-conscious setups.
IaC, Secret Detection, and the “Extra Credit” Round
This is where Trivy pulls away from the pack.
# Scan Terraform configs for misconfigurationstrivy config ./terraform/
# Scan Kubernetes manifeststrivy config ./k8s/
# Scan a Helm charttrivy config ./helm/my-chart/
# Scan a git repo for secrets and IaC issuestrivy repo https://github.com/your-org/your-repo
# Scan filesystem for secrets (finds .env files, API keys, etc.)trivy fs --scanners secret ./Grype doesn’t do IaC scanning — it’s focused on vulnerability detection. Scout doesn’t either. If you want one tool that handles CVEs and your Terraform state file that someone accidentally committed in 2023, Trivy wins by default.
Offline and Air-Gapped Environments
Home labbers running isolated networks will care about this.
Trivy has explicit air-gap support:
# Download DB on a connected machinetrivy image --download-db-only
# Copy the DB to your air-gapped machine# Default location: ~/.cache/trivy/db/
# Scan with local DB (skip update check)trivy image --skip-db-update --skip-java-db-update nginx:latestGrype also supports offline mode:
# Update DB on connected machinegrype db update
# Copy ~/.cache/grype/db/ to air-gapped machine# Disable auto-updateGRYPE_DB_AUTO_UPDATE=false grype nginx:latestScout is not designed for air-gapped use. It relies on Docker’s cloud infrastructure for most of its value. Running Scout offline is possible in theory but loses most useful features in practice.
Offline winner: Trivy — it has the most mature air-gap workflow and explicit documentation for it.
Cost
| Tool | Personal/Home Lab | CI (OSS/self-hosted) | Enterprise |
|---|---|---|---|
| Trivy | Free, OSS | Free | Free (Aqua Platform is separate/paid) |
| Grype | Free, OSS | Free | Free (Anchore Enterprise is separate/paid) |
| Docker Scout | Free (limited Hub images) | Free for public repos | Paid org plan required for policy, history, team features |
Trivy and Grype are genuinely free, forever. Scout is free until you want the features that make it worth using at scale. That’s not surprising — Docker needs revenue — but it shapes the decision for team environments.
The Verdict
Use Trivy if:
- You want one tool that does container scanning and IaC misconfiguration detection and secret scanning
- You’re running self-hosted CI (Gitea, Forgejo, Drone, Woodpecker)
- You have an air-gapped environment
- You want the widest CVE coverage and don’t mind tuning the noise with
--ignore-unfixed - This is your first scanner — Trivy’s docs are excellent and the community is huge
Use Grype if:
- You’re already in the Anchore ecosystem (Anchore Enterprise, Syft for SBOM generation)
- You want a clean separation between “generate SBOM” (Syft) and “check SBOM for CVEs” (Grype)
- You find Trivy’s output too noisy and want slightly more curated results
- Your CI pipeline already has Syft in it for compliance reasons
Use Docker Scout if:
- You live in Docker Desktop and want CVE context without leaving your GUI
- You’re running a small team on Docker Hub and want the integrated dashboard experience
- You want the cleanest “what base image update fixes this” guidance with minimal setup
- You’re okay with the paid tier for org-level features
Skip buying into Scout for serious CI scanning — the credential dependency and cloud reliance make it the wrong tool for automated pipelines that need to be reliable at 3 AM when Docker’s API has a blip.
And honestly? None of these replace patching. Knowing you have CVE-2024-whatever in your base image is only useful if you actually rebuild with an updated image. Scanner output without a rebuild + redeploy pipeline is just a longer to-do list. Set up automated base image rebuilds (Renovate Bot, Watchtower for non-critical services, or a weekly cron that pulls and rebuilds) alongside whatever scanner you pick.
The scanner tells you the building is on fire. You still have to pull the alarm.
Quick Reference
# Trivy — scan and only fail on fixable HIGH/CRITICALtrivy image --ignore-unfixed --severity HIGH,CRITICAL --exit-code 1 nginx:latest
# Grype — scan with Syft SBOM workflowsyft nginx:latest -o cyclonedx-json | grype --fail-on high
# Scout — quick overview with remediation hintsdocker scout recommendations nginx:latestPick your scanner, wire it into CI, and then go fix the actual vulnerabilities. The tool is the easy part.