Why Docker images are an entry point for attackers
SMBs are adopting Docker at scale — often through tools like Coolify to self-host their applications. That’s a sound operational decision. But there’s a common blind spot: public images pulled from Docker Hub bundle hundreds of system packages, and some of those packages contain known vulnerabilities.
This is what a supply-chain attack looks like in a container context. You’re not intentionally downloading malicious code — you pull node:18 or nginx:latest, and that image ships a version of OpenSSL with a critical flaw you never noticed. An attacker who exploits that flaw can potentially compromise your entire server.
Trivy is the practical answer to this problem. It’s a free, open-source scanner maintained by Aqua Security that analyzes your Docker images, configuration files, and application dependencies in seconds. No cybersecurity expertise required.
This tutorial is aimed at developers, DevOps practitioners, and SMB owners who manage their infrastructure without a dedicated security team.
Prerequisites
- Docker installed and running on your machine or server (Linux, macOS, or Windows with WSL2)
- A terminal with sufficient permissions to run Docker commands
- No prior security knowledge — Trivy installs in a single command
Step 1: Install Trivy
The simplest way to get started is to use the official Aqua Security install script.
On Linux or macOS:
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.59.1This command downloads and installs Trivy directly into /usr/local/bin. Pinning the version avoids surprises on future reinstalls.
No local install needed — run Trivy via Docker:
If you’d rather not install anything on the host machine, Trivy runs perfectly as a container:
docker run --rm aquasec/trivy:0.59.1 image nginx:latestThis approach is useful when you want to run a quick test on a production server without modifying the environment.
Verify the installation:
trivy --version# Trivy version: 0.59.1On the first scan, Trivy automatically downloads its CVE vulnerability database from the official repository. This takes 30 to 60 seconds depending on your connection. Subsequent runs use the local cache.
trivy image --download-db-only if you haven’t run a scan in several days. Step 2: Scan your first Docker image
Let’s start with a common image to understand the output format. Run a scan on nginx:latest:
trivy image nginx:latestThe report prints to the terminal. Here’s what each column means:
| Column | Description |
|---|---|
Library | The affected package or library |
Vulnerability ID | The official CVE identifier (e.g. CVE-2024-3596) |
Severity | Criticality level: CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN |
Installed Version | The version currently present in the image |
Fixed Version | The patched version available (empty if no fix exists) |
Title | Short description of the vulnerability |
Trivy separates vulnerabilities into two categories in its reports:
- OS packages: flaws in system packages (libc, openssl, curl…) provided by the image’s base distribution
- Application dependencies: flaws in your code’s dependencies (npm, pip, composer…)
OS vulnerabilities are often the most numerous but not always the most exploitable. Vulnerabilities in your application dependencies deserve priority attention because they directly affect your code.
Practical example with node:18:
trivy image node:18 --severity CRITICAL,HIGHFiltering with --severity immediately cuts the noise. On an outdated node:18 image, you’ll typically find CRITICAL CVEs on packages like libssl or zlib. The report shows the Fixed Version — that’s your target version to patch.
node:latest or nginx:latest in production without scanning the image first. The latest tag changes without warning and can introduce new vulnerabilities at any time. Step 3: Scan your local images and those deployed with Coolify
Trivy works equally well on images pulled from a registry and on locally built images. Before pushing an image to production:
trivy image my-app:latest --severity CRITICAL,HIGHScan all images running on your Coolify server:
Start by listing the images currently in use:
docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "<none>"Then automate the scan with a simple Bash script:
#!/bin/bash# Scan all Docker images and export results as JSON
OUTPUT_DIR="./trivy-reports/$(date +%Y-%m-%d)"mkdir -p "$OUTPUT_DIR"
docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "<none>" | while read image; do safe_name=$(echo "$image" | tr '/:' '--') echo "Scanning $image..." trivy image \ --severity CRITICAL,HIGH \ --format json \ --output "$OUTPUT_DIR/${safe_name}.json" \ "$image"done
echo "Reports exported to $OUTPUT_DIR"chmod +x scan-all-images.sh./scan-all-images.shReports are exported as JSON into a timestamped folder. To prioritize fixes, focus on entries where FixedVersion is not empty — those are the vulnerabilities you can patch right now.
/home/coolify or a dedicated folder to keep reports organized. Step 4: Fix the vulnerabilities Trivy found
Trivy found CRITICAL CVEs. Here are the three strategies to apply, in order.
Strategy 1 — Update the base image tag
This is the fastest fix. In your Dockerfile, replace the base image with a more recent version:
# BeforeFROM node:18
# After — Alpine image, lighter with a smaller attack surfaceFROM node:20-alpinenode:20-alpine ships Alpine Linux 3.x, which contains far fewer system packages than Debian or Ubuntu. Fewer packages means fewer potential vulnerabilities.
Strategy 2 — Switch to Distroless images
For production applications, Google’s Distroless images go even further than Alpine — they contain only the necessary runtime, with no shell or package manager:
FROM gcr.io/distroless/nodejs20-debian12COPY --from=build /app /appCMD ["/app/server.js"]Strategy 3 — Update your application dependencies
When Trivy flags vulnerabilities in your node_modules, requirements.txt, or composer.json, the fix happens at the code level:
# Node.jsnpm audit fix
# Pythonpip install --upgrade vulnerable-package
# PHPcomposer update vendor/vulnerable-packageRebuild and rescan:
docker build -t my-app:latest .trivy image my-app:latest --severity CRITICAL,HIGHIf the CRITICAL CVEs are gone, the fix is confirmed.
When no fix is available (Fixed Version is empty in the report), you can’t patch immediately. The right approach is to document the vulnerability in a SECURITY.md file, assess whether it’s actually exploitable in your specific context, and monitor the base image for updates.
node:18 to node:20-alpine in your Dockerfile is often enough to eliminate 80% of the CRITICAL CVEs Trivy identifies. Step 5: Automate scanning with a GitHub Action or a Docker cron job
Running Trivy manually is a good start. Integrating it into a CI/CD pipeline is what actually protects your infrastructure over time.
GitHub Actions — complete workflow file:
name: Trivy Security Scan
on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 6 * * 1' # Every Monday at 6am UTC
jobs: trivy-scan: name: Scan for vulnerabilities runs-on: ubuntu-latest
steps: - name: Checkout code uses: actions/checkout@v4
- name: Build Docker image run: docker build -t my-app:${{ github.sha }} .
- name: Scan with Trivy uses: aquasecurity/trivy-action@0.29.0 with: image-ref: my-app:${{ github.sha }} format: table exit-code: '1' # Fails the build if CRITICAL is detected severity: 'CRITICAL,HIGH' ignore-unfixed: true # Ignores CVEs with no available fixThe key parameter is exit-code: '1': if Trivy detects a CRITICAL or HIGH vulnerability with an available fix, the build fails automatically. The code never reaches production.
No CI/CD? Use a cron job on the server:
On a Coolify server without a GitHub pipeline, set up a weekly cron job:
# Add to crontab -e0 7 * * 1 /home/coolify/scan-all-images.sh 2>&1 | mail -s "Trivy Report $(date +%Y-%m-%d)" admin@your-domain.comTo send the report to Slack instead of email, replace the mail command with a curl call to an incoming Slack webhook.
Store reports to track progress over time:
The script from Step 3 already exports JSON files into timestamped folders (./trivy-reports/2026-02-25/). You can compare reports week over week to measure the reduction in CVE count — a concrete indicator of improving security posture.
ignore-unfixed: true to your automated scans to cut through the noise of CVEs with no available patch. Keep alerts focused on what’s actually actionable. Conclusion: Docker security within reach of any SMB
This tutorial covered the 5 essential steps: installing Trivy, scanning an image, reading the report, fixing vulnerabilities, and automating the whole process.
The concrete result: a Docker infrastructure that’s more resilient to supply-chain attacks, with no dedicated security budget or advanced expertise required. Switching from node:18 to node:20-alpine and wiring up a GitHub Action is about an hour of work that meaningfully reduces your attack surface.
Trivy goes well beyond image scanning. In a future article, we’ll explore how to use it to analyze Dockerfile configurations and Kubernetes manifests, and how to pair it with Falco for real-time threat detection on running containers.
Need help securing your Docker infrastructure or setting up a robust CI/CD pipeline? At Kodixar, I help SMBs implement DevSecOps practices that fit their size and budget.