Scan Docker Images With Trivy Before They Reach Your Homelab

Scan Docker Images With Trivy Before They Reach Your Homelab

Use Trivy to scan Docker images and Compose stacks before deployment. A practical security workflow for catching known container vulnerabilities.

💡 Disclosure: This article contains affiliate links. If you make a purchase through these links, we may earn a small commission at no extra cost to you. This helps support the site and keeps the content free.

A Docker image is not a sealed appliance. It is an operating system, language packages, and application code bundled together, often assembled by people you have never met. Pulling :latest because it worked last time is convenient. It is also how an old OpenSSL package or a forgotten Python dependency quietly lands on a server with your data.

Trivy is the scanner I would put in front of that habit. It is free, runs locally, understands container images and filesystem dependencies, and gives you a concrete reason to pause before docker compose up -d.

This is not a pitch for security theatre. A scanner will not stop a malicious maintainer, secure an exposed admin panel, or compensate for a Docker socket mounted into every container. It does catch known vulnerabilities early, which is a boring control that pays for itself the first time it keeps an obviously stale image out of production.

The short version

Use Trivy in two places:

  1. scan an image before adding or updating it in Compose;
  2. scan the whole stack directory in CI or before a manual deploy.

Start by failing only on high and critical findings with a fix available. If you fail every build on every low-severity advisory, the tool becomes background noise and gets removed on a busy evening. Security controls need to be strict enough to matter and practical enough to survive.

What Trivy actually checks

An image scan compares packages inside an image with vulnerability databases. That includes OS packages such as Alpine’s apk packages or Debian’s apt packages, plus language ecosystems where Trivy can identify them.

The result is a list of CVEs, their severity, the installed version, and usually a fixed version. That last field matters. A critical issue with no upstream fix deserves attention, but it is not the same deploy decision as a critical issue solved by changing an image tag.

Trivy can also scan a project directory for dependency issues, secrets, and infrastructure-as-code mistakes. For a Compose-based server, that gives you one command that can inspect the image references, the project files, and accidentally committed credentials.

Install Trivy without curl-piping a mystery script

Use the official installation instructions for your platform. On Debian or Ubuntu, installing the signed repository package is usually the least surprising option. On macOS, Homebrew is fine. On a CI runner, pin the action or package version rather than downloading whatever a URL serves today.

After installation, confirm the binary works:

trivy version

The first scan downloads vulnerability database data. Let it finish before deciding that the command is slow. Later scans use the local cache and are much less dramatic.

Scan a single Docker image first

Pick an image already in one of your Compose files. Here is an example with a versioned Caddy image:

trivy image caddy:2.8.4-alpine

Trivy prints a table similar to this:

Target: caddy:2.8.4-alpine (alpine 3.x)

Total: 3 (HIGH: 2, CRITICAL: 1)

Library   Vulnerability   Severity   Installed Version   Fixed Version
libcrypto CVE-20xx-xxxxx  HIGH       3.x.y-r0            3.x.y-r1

Do not treat the sample numbers above as a finding in that image. Your result depends on the exact tag and the current vulnerability database. The useful part is the workflow: scan the same immutable image reference you plan to deploy, then read the installed and fixed versions.

For a compact report that is easier to use in scripts:

trivy image \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  --exit-code 1 \
  caddy:2.8.4-alpine

--exit-code 1 makes the command fail when matching findings exist. --ignore-unfixed avoids blocking on vulnerabilities for which the upstream project has not released a patch. That is a policy choice, not a magic exemption: track unfixed critical issues separately when the exposed service is important.

Scan a Compose project, not just the shiny new app

Scanning one image is good. Scanning the directory where you keep your stack catches another class of mistakes, including secrets left in a .env file or risky configuration files.

From the directory containing docker-compose.yml or compose.yaml, run:

trivy fs \
  --scanners vuln,secret,misconfig \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  .

The filesystem scan does not replace scanning images. It examines files available in the directory, not the final filesystem layers from a remote registry. Run both when you are changing a real service.

Secrets scanning is especially useful before a repository becomes public by accident. It may produce false positives for examples and test credentials. Fix the real secret, rotate it if it was exposed, then use a narrowly scoped ignore rule for a deliberate example. Never silence the entire scanner because one tutorial contains password=example.

Put a hard gate before deployment

The deployment script below stops before Compose touches the server if Trivy finds a fixable high or critical image vulnerability. Replace the image list with the exact versions you use.

#!/usr/bin/env bash
set -euo pipefail

images=(
  "caddy:2.8.4-alpine"
  "postgres:16.4-alpine"
  "vaultwarden/server:1.32.0-alpine"
)

for image in "${images[@]}"; do
  echo "Scanning $image"
  trivy image \
    --severity HIGH,CRITICAL \
    --ignore-unfixed \
    --exit-code 1 \
    "$image"
done

trivy fs \
  --scanners vuln,secret,misconfig \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  .

docker compose pull
docker compose up -d

The ordering is intentional. Scan first, pull second, deploy last. If you scan a mutable tag and pull it later, you may deploy different bytes than the ones Trivy inspected. The better version of this script uses image digests in Compose, such as registry.example/app@sha256:....

That pairs nicely with signing Docker images with Cosign. Cosign answers whether an image came from a trusted signer. Trivy answers whether it contains known vulnerable components. Neither replaces the other.

Do not panic at every CVE

The least helpful response to a scanner is either ignoring it forever or rebuilding your stack every time it prints red text. Read the context:

  • Is the service exposed to the internet or reachable only on a private network?
  • Is the vulnerable component actually used by this image?
  • Is there a fixed version, and does the image maintainer ship it?
  • Does the advisory have a realistic exploit path in your setup?
  • Can you update without breaking a database or a major version boundary?

For an internet-facing reverse proxy or password manager, I patch high-severity issues with an available fix quickly. For an internal tool with an unfixed issue, I document the risk, reduce its network access, and watch upstream releases. Pretending both situations deserve identical urgency is how teams burn out.

A clean scan is not a clean bill of health either. Trivy only knows what has been disclosed and indexed. Keep your services updated, use separate Docker networks, avoid unnecessary ports, and make backups that you have actually restored.

My baseline policy for a small server

If you want one rule that is easy to remember, use this:

  • never deploy floating latest tags for services that hold data;
  • scan new images and planned upgrades for high and critical vulnerabilities;
  • block automatically only on findings with a fix, then review the rest;
  • scan your Compose directory for secrets before pushing it anywhere;
  • use image digests and signatures when the service is sensitive.

That is enough discipline for a homelab or small VPS without converting it into a compliance department. The goal is not a dashboard full of zeroes. The goal is knowing what you are running before it gets access to your network, volumes, and credentials.

Secure the management path too

A perfect image scan does not help if you deploy from an untrusted network or expose your management dashboards to the public internet. Keep tools such as Portainer, Grafana, and your SSH access behind a private access layer.

🚀NordVPN

Secure your server with a reliable VPN.

Get NordVPN →

Affiliate link — we may earn a commission at no extra cost to you.

A VPN is not a replacement for updates or scanning. It is one more useful boundary around the machine that holds your credentials and deployment access.

What to do now

Run trivy image against one image from your current Compose file. If it has a fixed high or critical finding, update the tag, scan again, and record the change in Git. If it is clean, add the scan to your deploy script while the habit is fresh.

The best time to discover a vulnerable container is before it starts. Trivy gives you that chance with a command small enough that there is no excuse to skip it.

Stay in the loop 📬

Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.

Join 0 self-hosters. Free forever.