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 Image | Size | Packages | Typical CVEs |
|---|---|---|---|
| node:20 | 1.1 GB | 420+ | 15-30 |
| node:20-slim | 240 MB | 120+ | 5-12 |
| node:20-alpine | 180 MB | 50+ | 3-8 |
| distroless/nodejs20 | 130 MB | 15 | 0-2 |
| chainguard/node | 95 MB | 8 | 0-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.
Sarah Chen
Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.