BlogCybersecurity
Cybersecurity

Docker Container Security Scanning in 2026: Trivy, Grype, and Building Secure Base Images

Your Docker image has 347 vulnerabilities and you have no idea. This guide covers automated vulnerability scanning with Trivy and Grype, building minimal secure base images with distroless and Chainguard, runtime security with Falco, and integrating scanning into CI/CD so vulnerable images never reach production.

S

Sarah Chen

Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.

February 9, 2026
21 min read

In December 2025, a critical vulnerability in a popular Node.js base image (node:20-alpine) went unpatched for 11 days. Any application built on that image during those 11 days shipped with a known remote code execution vulnerability. The teams that caught this immediately had automated container scanning in their CI/CD pipeline. The teams that did not discover it until weeks later, after the image was running in production serving customer traffic.

Container images are not static artifacts — they are composed of hundreds of packages, each with its own vulnerability lifecycle. A "clean" image today can have critical CVEs tomorrow. This guide covers the complete container security lifecycle: scanning, building secure base images, runtime monitoring, and CI/CD integration with real configurations you can deploy immediately.

The Container Vulnerability Landscape in 2026

A typical Node.js application image based on node:20 (Debian Bookworm) contains over 400 installed packages. On average, 15-25 of those packages have known CVEs at any given time. Even the "slim" variants (node:20-slim) contain 100+ packages with 5-10 CVEs. Alpine-based images (node:20-alpine) are smaller but still contain vulnerabilities in musl libc, busybox, and apk-tools.

The vulnerability categories in container images break down as follows:

  • OS-level packages: openssl, zlib, glibc/musl, curl, etc. — these are the most common and typically the most critical because they affect network-facing functionality
  • Language runtime: Node.js, Python, Go, Java — runtime CVEs often enable remote code execution
  • Application dependencies: npm packages, pip packages, Go modules — these are scanned from lockfiles (package-lock.json, requirements.txt, go.sum)
  • Configuration issues: running as root, writable filesystems, excessive capabilities, exposed secrets in image layers

Scanning with Trivy: The Standard Tool

Trivy (by Aqua Security) has become the de facto standard for container vulnerability scanning. It scans OS packages, language dependencies, IaC files, and secrets in a single tool. It is fast (scans a typical image in 5-15 seconds after the first DB download), accurate (low false positive rate), and integrates with every major CI/CD system:

# Install Trivy:
# macOS:
brew install trivy

# Linux (official repo):
sudo apt-get install wget apt-transport-https gnupg
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 generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy -y

# Scan a local image:
trivy image myapp:latest

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

# Scan and fail if any CRITICAL vulnerabilities found (for CI/CD):
trivy image --exit-code 1 --severity CRITICAL myapp:latest

# Output as JSON for programmatic processing:
trivy image --format json --output results.json myapp:latest

# Scan a Dockerfile before building (IaC scanning):
trivy config Dockerfile

# Scan for exposed secrets in image layers:
trivy image --scanners secret myapp:latest

# Full comprehensive scan:
trivy image --scanners vuln,secret,misconfig --severity CRITICAL,HIGH myapp:latest

Scanning with Grype: The Alternative

Grype (by Anchore) is an excellent alternative to Trivy with different vulnerability database sources. Running both tools catches vulnerabilities that either one might miss due to database timing differences:

# Install Grype:
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# Generate an SBOM first with Syft (Grype's companion tool):
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
syft myapp:latest -o json > sbom.json

# Scan the SBOM:
grype sbom:sbom.json

# Or scan an image directly:
grype myapp:latest

# Only show fixable vulnerabilities (actionable results):
grype myapp:latest --only-fixed

# Fail on critical vulnerabilities:
grype myapp:latest --fail-on critical

Building Secure Base Images: Distroless and Chainguard

The most effective way to reduce vulnerabilities is to reduce the attack surface. Instead of starting from node:20 (400+ packages) or even node:20-alpine (50+ packages), use distroless or Chainguard images that contain ONLY your application runtime — no shell, no package manager, no coreutils:

# Distroless Node.js image (Google):
# Multi-stage build — build in full image, run in distroless:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs20-debian12:nonroot
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["dist/server.js"]

# Chainguard Node.js image (even fewer packages):
FROM cgr.dev/chainguard/node:latest AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
ENTRYPOINT ["node", "dist/server.js"]

Vulnerability count comparison for the same Node.js application:

Base ImageSizePackagesTypical CVEs
node:201.1 GB420+15-30
node:20-slim240 MB120+5-12
node:20-alpine180 MB50+3-8
distroless/nodejs20130 MB150-2
chainguard/node95 MB80-1

CI/CD Integration: GitHub Actions Pipeline

name: Container Security Scan
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      security-events: write  # For GitHub Security tab
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:ci .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:ci'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Run Grype as secondary scanner
        uses: anchore/scan-action@v4
        with:
          image: 'myapp:ci'
          fail-build: true
          severity-cutoff: critical
          output-format: sarif

      - name: Scan Dockerfile for misconfigurations
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          exit-code: '1'

Runtime Security with Falco

Scanning images before deployment catches known vulnerabilities, but runtime security monitors containers for suspicious behavior during execution — detecting zero-day exploits, container escapes, and lateral movement that static scanning cannot detect:

# Install Falco on the host:
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | \
  sudo gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" | \
  sudo tee /etc/apt/sources.list.d/falcosecurity.list
sudo apt-get update && sudo apt-get install -y falco

# Custom Falco rules for container security:
# /etc/falco/rules.d/custom-container-rules.yaml
- rule: Shell Spawned in Container
  desc: Detect shell execution inside a container (indicates compromise)
  condition: >
    spawned_process and
    container and
    proc.name in (bash, sh, zsh, ash, dash) and
    not container.image.repository in (my-debug-image)
  output: "Shell spawned in container (container=%container.name image=%container.image.repository cmd=%proc.cmdline)"
  priority: WARNING

- rule: Outbound Connection from Non-Web Container
  desc: Detect unexpected outbound connections
  condition: >
    outbound and
    container and
    not fd.sport in (80, 443, 8080, 3000, 5432, 6379) and
    not container.image.repository in (curl-job, backup-agent)
  output: "Unexpected outbound connection (container=%container.name image=%container.image.repository connection=%fd.name)"
  priority: NOTICE

Container security is not a single tool — it is a pipeline. Scan images before they enter your registry, scan again before deployment, monitor at runtime, and automate the entire process so no human decision-making is required. The goal is zero critical vulnerabilities in production, verified continuously. ZeonEdge implements container security pipelines for teams that need to ship fast without shipping vulnerabilities. Explore our container security services.

S

Sarah Chen

Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.