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 .
Tip: Use 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:

  1. Scan every image before deployment — block deployments with CRITICAL vulnerabilities
  2. Rescan production images weekly — catch newly discovered CVEs
  3. Triage by severity — CRITICAL = fix within 24 hours, HIGH = fix within 1 week, MEDIUM = fix within 1 month
  4. Track exceptions — use .trivyignore or similar files to document accepted risks
  5. Automate base image updates — use Dependabot or Renovate to automatically propose base image updates
  6. 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.

Next step: Run your first scan right now:
docker run --rm aquasec/trivy image your-production-image:latest
You might be surprised what you find.