Docker Security Scanning: How to Find and Fix Container Vulnerabilities
Every Docker image you pull from a registry is a black box of dependencies. That node:18-alpine base image? It contains hundreds of packages, each with its own dependency tree, each potentially harboring known vulnerabilities. The python:3.12-slim image you use for your API? Same story.
Container security scanning is the process of analyzing Docker images to identify known vulnerabilities (CVEs) in their installed packages, libraries, and binaries. It's not optional for production workloads; it's a baseline security practice that catches problems before they reach your users.
This guide covers the why, the tools, the how, and most importantly, what to do when vulnerabilities are found.
Why Container Security Scanning Matters
Here's a reality check: a typical Docker image based on Ubuntu or Debian contains 400-600 installed packages. Even minimal Alpine-based images have 30-50 packages. Each of these packages is a potential attack vector if it contains a known vulnerability.
The numbers are sobering. According to vulnerability databases, even official Docker images regularly contain dozens of known CVEs at any given time. Most are low or medium severity, but critical vulnerabilities do appear and they need to be addressed quickly.
What Scanners Actually Check
- OS packages — apt, apk, yum packages installed in the image
- Language-specific dependencies — npm packages, Python pip packages, Go modules, Java JARs
- Application binaries — compiled executables with known vulnerabilities
- Configuration issues — some scanners also check for misconfigurations (running as root, exposed secrets)
The Top Container Scanning Tools
Trivy (Recommended)
Trivy, developed by Aqua Security, has become the de facto standard for container vulnerability scanning. It's open source, fast, and comprehensive.
# Install Trivy
# On macOS
brew install trivy
# On Ubuntu/Debian
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy
# Scan an image
trivy image nginx:latest
# Scan with only HIGH and CRITICAL severity
trivy image --severity HIGH,CRITICAL nginx:latest
# Scan a local Dockerfile
trivy config Dockerfile
# Output as JSON for CI/CD processing
trivy image --format json --output results.json nginx:latest
Trivy's key strengths:
- Fast scanning with a local vulnerability database (no API calls per scan)
- Covers OS packages and language-specific dependencies
- Also scans IaC files (Terraform, CloudFormation), Kubernetes manifests, and Dockerfiles
- Multiple output formats: table, JSON, SARIF, CycloneDX, SPDX
- No database server required; runs entirely as a CLI tool
Grype
Grype, developed by Anchore, is another excellent open-source scanner. It's particularly good if you want SBOM (Software Bill of Materials) integration through its sister project, Syft.
# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Scan an image
grype nginx:latest
# Only show fixable vulnerabilities
grype nginx:latest --only-fixed
# Output as JSON
grype nginx:latest -o json
# Generate SBOM first with Syft, then scan
syft nginx:latest -o json > sbom.json
grype sbom:sbom.json
Grype's advantages:
- Tight integration with Syft for SBOM generation
- Fast scanning with offline vulnerability database
- Excellent filtering options (by severity, fixability)
- Supports scanning OCI archives and directories
Snyk Container
Snyk is a commercial platform with a generous free tier. Unlike Trivy and Grype, Snyk provides continuous monitoring: it alerts you when new vulnerabilities are discovered in images you've already scanned.
# Install Snyk CLI
npm install -g snyk
# Authenticate
snyk auth
# Scan an image
snyk container test nginx:latest
# Monitor an image (get alerts for new CVEs)
snyk container monitor nginx:latest
# Scan with a specific Dockerfile for better remediation advice
snyk container test nginx:latest --file=Dockerfile
Snyk's advantages:
- Continuous monitoring with email/Slack alerts
- Remediation advice (suggests base image upgrades)
- Integration with GitHub, GitLab, Docker Hub
- Free tier: 200 container tests per month
Tool Comparison
| Feature | Trivy | Grype | Snyk |
|---|---|---|---|
| License | Apache 2.0 | Apache 2.0 | Freemium |
| OS Packages | Yes | Yes | Yes |
| Language Deps | Yes | Yes | Yes |
| IaC Scanning | Yes | No | Separate product |
| SBOM Generation | Yes | Via Syft | Yes |
| Continuous Monitoring | No (run manually) | No (run manually) | Yes |
| CI/CD Integration | Excellent | Good | Excellent |
| Scan Speed | Fast | Fast | Moderate (API calls) |
| Offline Mode | Yes | Yes | No |
Integrating Scanning into Your Workflow
Pre-Commit: Scan During Build
The earliest you can catch vulnerabilities is during the image build process. Add scanning as a build step in your Dockerfile or build script:
#!/bin/bash
# build-and-scan.sh
IMAGE_NAME="myapp:$(git rev-parse --short HEAD)"
# Build the image
docker build -t "$IMAGE_NAME" .
# Scan it
trivy image --exit-code 1 --severity CRITICAL "$IMAGE_NAME"
if [ $? -ne 0 ]; then
echo "CRITICAL vulnerabilities found. Fix before pushing."
exit 1
fi
echo "Scan passed. Pushing image..."
docker push "$IMAGE_NAME"
CI/CD Pipeline Integration
Here's how to add Trivy scanning to common CI/CD platforms:
GitHub Actions:
# .github/workflows/scan.yml
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- 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'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
GitLab CI:
# .gitlab-ci.yml
container_scanning:
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity CRITICAL,HIGH "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
allow_failure: false
Scheduled Scanning
Vulnerabilities are discovered after images are deployed. Set up a cron job or scheduled pipeline to rescan your production images regularly:
# crontab entry: scan production images daily at 6 AM
0 6 * * * /usr/local/bin/trivy image --severity HIGH,CRITICAL myapp:production 2>&1 | mail -s "Daily Container Scan" [email protected]
# Or as a Docker container
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image --severity HIGH,CRITICAL myapp:production
Scanning with a Management Platform
If you use a Docker management platform like usulnet that has built-in security scanning, you get scanning integrated directly into your container management workflow. No separate tool to install, no additional pipeline to configure. You can scan any image from the UI and see results alongside your container status and resource metrics.
Interpreting Scan Results
A typical Trivy scan output looks like this:
$ trivy image python:3.12-slim
python:3.12-slim (debian 12.4)
Total: 85 (UNKNOWN: 0, LOW: 52, MEDIUM: 25, HIGH: 7, CRITICAL: 1)
┌──────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬──────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├──────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼──────────────────────────────────┤
│ libssl3 │ CVE-2024-XXXXX │ CRITICAL │ fixed │ 3.0.11-1~deb12u2 │ 3.0.13-1~deb │ openssl: buffer overflow in... │
│ libexpat1 │ CVE-2024-XXXXX │ HIGH │ fixed │ 2.5.0-1 │ 2.5.0-1+deb │ expat: XML parsing vulnerability │
│ zlib1g │ CVE-2024-XXXXX │ MEDIUM │ fixed │ 1:1.2.13-1 │ 1:1.2.13-2 │ zlib: heap buffer overflow │
└──────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴──────────────────────────────────┘
Understanding Severity Levels
- CRITICAL (CVSS 9.0-10.0) — remotely exploitable vulnerabilities that can lead to full system compromise. Fix immediately.
- HIGH (CVSS 7.0-8.9) — significant vulnerabilities that should be fixed in the next release cycle.
- MEDIUM (CVSS 4.0-6.9) — vulnerabilities that require specific conditions to exploit. Plan to fix.
- LOW (CVSS 0.1-3.9) — minor issues with limited impact. Fix when convenient.
The "Fixed Version" Column
This is the most actionable piece of information. If a fixed version exists, you can resolve the vulnerability by updating the package. If the status shows "not fixed" or "won't fix," you need a different strategy (see the remediation section below).
Fixing Vulnerabilities
Strategy 1: Update the Base Image
The most effective fix for OS-level vulnerabilities is updating your base image. Image maintainers regularly release patches:
# Instead of a pinned old version
FROM python:3.12.1-slim
# Use the latest patch version
FROM python:3.12-slim
# Or pin to a specific patched version
FROM python:3.12.2-slim
# Rebuild
docker build --no-cache -t myapp:latest .
docker build --no-cache when updating base images to ensure Docker doesn't use cached layers with old packages.
Strategy 2: Update Packages in the Dockerfile
If the base image hasn't been updated yet, you can update specific packages:
FROM python:3.12-slim
# Update vulnerable packages explicitly
RUN apt-get update && \
apt-get install -y --only-upgrade libssl3 libexpat1 && \
rm -rf /var/lib/apt/lists/*
# ... rest of your Dockerfile
Strategy 3: Switch to a Smaller Base Image
Fewer packages means fewer potential vulnerabilities. Consider switching to a more minimal base image:
# Instead of full Debian-based image (400+ packages)
FROM python:3.12
# Use slim variant (150+ packages)
FROM python:3.12-slim
# Or Alpine-based (30+ packages)
FROM python:3.12-alpine
# Or distroless (minimal packages, no shell)
FROM gcr.io/distroless/python3-debian12
Strategy 4: Multi-Stage Builds
Use multi-stage builds to keep build dependencies out of your production image:
# Build stage: has compilers, dev headers, etc.
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# Production stage: minimal image
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
Strategy 5: Accept the Risk (With Documentation)
Not every vulnerability is exploitable in your context. A vulnerability in libcurl doesn't matter if your container never makes outbound HTTP requests. When you decide to accept a risk:
# Create a .trivyignore file
# .trivyignore
# CVE-2024-XXXXX: libcurl vulnerability - not exploitable in our context
# because this container has no outbound network access.
# Reviewed by: security-team on 2025-02-08
CVE-2024-XXXXX
Always document why a vulnerability is being accepted and who made the decision.
Building a Vulnerability Management Process
Scanning is only useful if you act on the results. Here's a practical process:
- Scan every image before deployment — block deployments with CRITICAL vulnerabilities
- Rescan production images weekly — catch newly discovered CVEs
- Triage by severity — CRITICAL = fix within 24 hours, HIGH = fix within 1 week, MEDIUM = fix within 1 month
- Track exceptions — use
.trivyignoreor similar files to document accepted risks - Automate base image updates — use Dependabot or Renovate to automatically propose base image updates
- Generate SBOMs — maintain a software bill of materials for each production image for compliance and incident response
Conclusion
Container security scanning isn't a one-time task. It's an ongoing practice that needs to be embedded into your development and deployment workflow. Start with Trivy for its simplicity and zero-cost, add it to your CI/CD pipeline, and build from there.
If you're looking for a Docker management platform with built-in security scanning, usulnet integrates vulnerability scanning directly into the container management workflow, so your team can see security status alongside container health without switching tools.
docker run --rm aquasec/trivy image your-production-image:latest
You might be surprised what you find.