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.
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:
- CI pipeline — fail the build if any CRITICAL CVEs exist with a fix available
- Weekly cron on my homelab — scan all running containers, alert if new CVEs appeared
- 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
.trivyignoreto 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.
📧 Get weekly insights on security, trading, and tech. No spam, unsubscribe anytime.
Leave a Reply