Container Security Scanning and Vulnerability Assessment in Docker

Container Security Scanning and Vulnerability Assessment in Docker

We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.

When I first moved my homelab to Docker, I thought image security was someone else's problem. Then I pulled a popular application image with 47 known vulnerabilities. That's when I realized: scanning containers isn't optional—it's foundational. I'll walk you through setting up container vulnerability scanning that actually catches real problems before they hit production.

Why Container Scanning Matters for Homelabs

Docker images are like onions: layers upon layers of dependencies. A single image might contain hundreds of third-party packages, each with its own security history. When you run docker pull nginx:latest, you're not just getting a web server—you're inheriting all of its dependencies and their vulnerability records.

I prefer Trivy for most homelab work because it's free, fast, and requires no network calls or API keys. Unlike some commercial scanners, Trivy downloads its vulnerability database once and scans locally. On a VPS (even a budget option like RackNerd's ~$40/year public VPS), scanning runs in seconds without eating bandwidth.

Scanning solves three problems: discovery (what's actually vulnerable in your image), assessment (how critical is it), and prioritization (which fixes matter most). Without it, you're flying blind.

Installing and Running Trivy Locally

Trivy installs as a single binary. I prefer running it on my VPS or homelab Docker host so I can scan everything in one place.

# On Ubuntu/Debian
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

# Verify installation
trivy --version

Once installed, scan an image directly from Docker Hub or a local image:

# Scan a local image
trivy image nginx:latest

# Scan and output JSON for parsing
trivy image --format json --output results.json nginx:latest

# Scan with severity filter (only show HIGH and CRITICAL)
trivy image --severity HIGH,CRITICAL nginx:latest

# Scan an image you've built locally
docker build -t myapp:1.0 .
trivy image myapp:1.0

The output shows vulnerability ID, severity, package name, installed version, and fixed version. For example:

bash CVE-2024-12345 | CRITICAL | openssl | 1.1.1a | 1.1.1w

This tells me: there's a critical vulnerability in OpenSSL 1.1.1a; I need to upgrade to 1.1.1w or later.

Tip: Create a simple scan script to run daily on your VPS. Add it to cron and email results. Trivy can export SARIF format, which integrates with GitHub/GitLab security dashboards.

Automating Scans in Docker Compose

For multi-container applications, I automate scanning as part of the build pipeline. Here's a Docker Compose setup with a dedicated scanner service:

version: '3.9'
services:
  app:
    build: .
    container_name: myapp
    ports:
      - "3000:3000"
    networks:
      - app-net

  trivy-scanner:
    image: aquasec/trivy:latest
    container_name: trivy-scan
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./scan-reports:/reports
    command: >
      image --format json --output /reports/myapp-scan.json
      --severity HIGH,CRITICAL
      myapp:latest
    depends_on:
      - app
    networks:
      - app-net

networks:
  app-net:
    driver: bridge

Run this with docker-compose up --build. The scanner service starts after your app builds, scans the image, and saves results to ./scan-reports/myapp-scan.json. If you want to fail the build on critical vulnerabilities, add a health check or wrapper script.

Alternative Scanners: Grype and Snyk

Trivy is my go-to, but I'll mention the alternatives I've tested.

Grype (from Anchore) is another excellent free option. It supports more package ecosystems (Python, Go, Ruby) than Trivy and has a slightly cleaner report format. Install with curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin, then scan with grype myimage:tag.

Snyk offers free tier scanning with web dashboard integration. The catch: you need a Snyk account and API key. For homelabs, it's overkill unless you're already using their other tools. Their free tier allows 200 monthly tests, which is reasonable for small labs.

I stick with Trivy for speed and privacy—no cloud calls, no account, no rate limits.

Interpreting Scan Results and Fixing Vulnerabilities

A typical scan output lists 10–100+ vulnerabilities. The key is separating signal from noise.

Severity ratings: CRITICAL (exploitable, immediate action), HIGH (likely exploitable), MEDIUM (possible exploitation), LOW (hard to exploit or requires special conditions), UNKNOWN (data missing).

I prioritize this way:

Fix vulnerabilities by updating base images. If your Dockerfile starts with FROM ubuntu:20.04, upgrade to FROM ubuntu:24.04 or FROM ubuntu:22.04 (LTS). Rebuild and rescan:

# Update Dockerfile
sed -i 's/FROM ubuntu:20.04/FROM ubuntu:24.04/' Dockerfile

# Rebuild and scan
docker build -t myapp:2.0 .
trivy image myapp:2.0

Sometimes vulnerabilities exist in third-party packages you don't control. In those cases, use trivy image --skip-update to see if the newer package version (that you're pulling from apt/pip) already has the fix. If it does, a fresh build will solve it.

Watch out: Don't ignore "UNKNOWN" severity issues. They often turn into CRITICAL once the CVE is fully indexed. Check the CVE details on NVD (nvd.nist.gov) if Trivy can't score it.

Integrating Scanning into Your VPS Workflow

For a production VPS (like a budget RackNerd instance at ~$40/year), I set up automated daily scans on all running containers:

#!/bin/bash
# /usr/local/bin/daily-container-scan.sh

REPORT_DIR="/home/user/scan-reports"
mkdir -p "$REPORT_DIR"
DATE=$(date +%Y-%m-%d)

# List all running container image IDs
docker images --format "{{.Repository}}:{{.Tag}}" | while read image; do
  echo "[$(date)] Scanning $image..."
  trivy image --format json --severity HIGH,CRITICAL \
    "$image" > "$REPORT_DIR/${image//\//-}_${DATE}.json"
done

# Alert on new CRITICAL vulnerabilities
find "$REPORT_DIR" -name "*${DATE}*.json" -exec grep -l '"Severity":"CRITICAL"' {} \; | \
  while read file; do
    echo "⚠ CRITICAL vulnerability found in $file" | mail -s "Container Scan Alert" [email protected]
  done

echo "[$(date)] Scan complete."

Add this to cron:

0 2 * * * /usr/local/bin/daily-container-scan.sh

Runs daily at 2 AM, emails critical alerts. Adjust severity and recipients to match your risk tolerance.

Scanning Private Registry Images

If you host images on a private registry (Harbor, Artifactory, or even Docker Hub private repos), Trivy can scan them with authentication:

# Scan private registry image
trivy image --username myuser --password mypass \
  private-registry.local:5000/myapp:v1.2.3

# Or use environment variable
export TRIVY_USERNAME=myuser
export TRIVY_PASSWORD=mypass
trivy image private-registry.local:5000/myapp:v1.2.3

For CI/CD pipelines, use Docker credentials file instead of environment variables for better security.

Keeping Vulnerability Databases Updated

Trivy downloads vulnerability data on first run. Update it regularly to catch newly disclosed CVEs:

# Update vulnerability database
trivy image --download-db-only

# Add to weekly cron job
0 3 * * 0 trivy image --download-db-only

Without updates, you'll only catch vulnerabilities disclosed before your last database pull. New CVEs appear constantly, so schedule this weekly at minimum.

Creating a Vulnerability Report Dashboard

For serious homelabs, aggregate scan results into a readable format. I convert JSON to HTML for quick review:

#!/bin/bash
# scan-report.sh - Convert Trivy JSON to HTML

REPORT_DIR="/home/user/scan-reports"
OUTPUT="$REPORT_DIR/index.html"

cat > "$OUTPUT" << 'EOF'



  Container Vulnerability Report
  


Container Vulnerability Report

Generated: $(date)

EOF # Parse JSON files and build table echo "" >> "$OUTPUT" for file in "$REPORT_DIR"/*.json; do [ -f "$file" ] || continue jq -r '.Results[]?.Misconfigurations[]? | select(.Severity=="CRITICAL" or .Severity=="HIGH") | "\(.Type) | \(.ID) | \(.Severity) | \(.Title)"' "$file" >> "$OUTPUT" done echo "
ImageCVESeverityPackage
" >> "$OUTPUT" echo "Report: $OUTPUT"

Run this weekly and open scan-reports/index.html in your browser for an at-a-glance view of all vulnerabilities across your homelab.

Real-World Example: Securing a Nextcloud Container

Let me show a practical fix. I scanned Nextcloud and found several HIGH/CRITICAL issues in the PHP base image:

Before: FROM php:7.4-apache

Trivy found: 23 CRITICAL, 15 HIGH vulnerabilities.

After: FROM php:8.2-apache

Rescan: 3 CRITICAL, 8 HIGH. Much better. Then I added security headers and enabled SELinux in the Dockerfile:

FROM php:8.2-apache

# Install security updates
RUN apt-get update && apt-get upgrade -y && apt-get clean

# Disable dangerous modules
RUN a2dismod status && a2dismod info

# Enable security headers
RUN a2enmod headers
RUN echo 'Header always set X-Content-Type-Options: nosniff' >> /etc/apache2/apache2.conf
RUN echo 'Header always set X-Frame-Options: SAMEORIGIN' >> /etc/apache2/apache2.conf

# Set file permissions strictly
RUN chown -R www-data:www-data /var/www/html
RUN chmod 750 /var/www/html

WORKDIR /var/www/html

Rebuilt and rescanned. Down to 2 CRITICAL (unrelated to our control), 6 HIGH. Acceptable