How to Scan Docker Images for Vulnerabilities with Trivy (Free, 5 Minutes)

Affiliate disclosure: This post contains affiliate links. If you buy through them I earn a commission at no extra cost to you. As an Amazon Associate I earn from qualifying purchases.

Last month I pulled a random Node.js base image for a side project and ran Trivy against it out of curiosity. 47 CVEs. Twelve of them critical. The image had been on Docker Hub for two years with 10M+ pulls. Nobody checks these things.

If you’re running containers — homelab, production, doesn’t matter — you need a vulnerability scanner. I’ve tried Snyk, Grype, Docker Scout, and Trivy. Trivy wins for my workflow because it’s a single binary, no account required, and scans in seconds.

On this page
  1. What Trivy Actually Does
  2. Install and First Scan
  3. Reading the Output
  4. My Actual Workflow
  5. Alpine vs Debian vs Distroless: The Numbers
  6. CI Integration (GitHub Actions)
  7. Things That Surprised Me
  8. Alternatives I’ve Tried
  9. Getting Started Today

What Trivy Actually Does#

Trivy is an open-source security scanner from Aqua Security. It checks container images, filesystems, git repos, and Kubernetes clusters against the NVD (National Vulnerability Database) and vendor-specific advisory feeds. It downloads its vulnerability DB on first run (~30MB) and updates it automatically.

The key thing: it checks every layer of your image. That old libssl in your base image from 2023? Trivy finds it. That npm package with a known prototype pollution bug? Found. Java Log4j still lurking somewhere? Found.

Install and First Scan#

On any Linux box (or macOS with Homebrew):

# Linux
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.50.0

# macOS
brew install trivy

# Verify
trivy --version

Your first scan takes about 30 seconds (DB download), subsequent scans take 3-8 seconds depending on image size:

# Scan any image
trivy image node:18-alpine

# Scan with only HIGH and CRITICAL
trivy image --severity HIGH,CRITICAL nginx:latest

# Scan a local Dockerfile build
docker build -t myapp:latest .
trivy image myapp:latest

Reading the Output#

Here’s what a real scan looks like against node:18-bullseye (I ran this today):

node:18-bullseye (debian 11.9)
Total: 312 (UNKNOWN: 2, LOW: 186, MEDIUM: 87, HIGH: 31, CRITICAL: 6)

┌──────────────────┬────────────────┬──────────┬────────────────────┐
│ Library          │ Vulnerability  │ Severity │ Fixed Version      │
├──────────────────┼────────────────┼──────────┼────────────────────┤
│ libssl1.1        │ CVE-2024-0727  │ HIGH     │ 1.1.1w-0+deb11u2   │
│ zlib1g           │ CVE-2023-45853 │ CRITICAL │ 1:1.2.11.dfsg-2+d2 │
│ curl             │ CVE-2024-2398  │ HIGH     │ 7.74.0-1.3+deb11u  │
└──────────────────┴────────────────┴──────────┴────────────────────┘

The “Fixed Version” column tells you if there’s a patch available. If it says “No fix,” you either accept the risk or switch base images.

My Actual Workflow#

I run Trivy in three places:

  1. CI pipeline — fail the build if any CRITICAL CVEs exist with a fix available
  2. Weekly cron on my homelab — scan all running containers, alert if new CVEs appeared
  3. Before pulling new images — quick sanity check

Here’s the cron script I use on my TrueNAS box:

#!/bin/bash
# scan-all-containers.sh
RESULTS=""
for img in $(docker ps --format '{{.Image}}' | sort -u); do
  OUTPUT=$(trivy image --severity HIGH,CRITICAL --quiet "$img" 2>/dev/null)
  if [ -n "$OUTPUT" ]; then
    RESULTS+="\n=== $img ===\n$OUTPUT\n"
  fi
done

if [ -n "$RESULTS" ]; then
  curl -d "Container vulnerabilities found: $RESULTS" ntfy.sh/my-alerts
fi

Alpine vs Debian vs Distroless: The Numbers#

I scanned the same Node.js app with three base images. Results from May 2026:

Base Image Size Total CVEs Critical High
node:18-bullseye 987MB 312 6 31
node:18-alpine 177MB 4 0 1
gcr.io/distroless/nodejs18 118MB 0 0 0

The pattern is clear: smaller image = fewer packages = fewer vulnerabilities. I switched everything I could to Alpine two years ago, and my scan noise dropped by 95%. Distroless is even better if you don’t need a shell for debugging.

CI Integration (GitHub Actions)#

- 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'

The exit-code: 1 means your build fails if vulnerabilities are found. The SARIF output integrates with GitHub’s Security tab so you can track issues over time.

Things That Surprised Me#

A few gotchas after running Trivy for 18 months:

  • Official images aren’t safe. The “official” Python, Node, and Ruby images on Docker Hub often ship with dozens of known CVEs. The maintainers can’t rebuild fast enough.
  • Alpine’s musl libc is a trade-off. Fewer CVEs but occasional compatibility issues with native Node modules. Test your app.
  • False positives exist. Some CVEs in libraries aren’t exploitable in your context. Use .trivyignore to suppress known-safe findings.
  • The DB update matters. If your CI caches the Trivy DB too aggressively, you’ll miss new CVEs. I refresh weekly minimum.

Alternatives I’ve Tried#

Grype (by Anchore) — similar speed, slightly different DB sources. Good second opinion but I prefer Trivy’s broader scanning scope (it does IaC too).

Docker Scout — built into Docker Desktop now. Convenient but requires a Docker account and only works with Docker images (no filesystem scanning).

Snyk Container — excellent fix suggestions but free tier is limited to 200 scans/month. For a homelab with 30+ containers, that burns fast.

Getting Started Today#

Install Trivy, scan your most critical container, and see what falls out. I guarantee you’ll find something you didn’t know about. If you’re running a homelab, the weekly cron approach catches drift before it becomes a problem.

For monitoring your homelab network alongside container security, a good network switch with VLAN support helps isolate container traffic. I use a TP-Link managed switch (affiliate link) to segment my container host from IoT devices — one compromised container shouldn’t reach my cameras.

If you’re running regex-based security checks in your code, check out our regex security patterns post — handy for validating input sanitization patterns. And if you’re handling untrusted documents in your pipeline, Dangerzone is worth a look.

Join Alpha Signal for free market intelligence — we track tech infrastructure trends alongside trading signals.