Docker Security: Container Scanning, Image Hardening & Runtime Protection

SCRs Team
March 22, 2026
15 min read
258 words
Share

Why Docker Security Matters More Than Ever

87% of organizations run containers in production (CNCF 2025 Survey), but most container images have critical vulnerabilities that never get fixed.

FindingStat
Images with critical CVEs73%
Images running as root62%
Images with unnecessary packages89%
Containers with writable root filesystem54%
Teams scanning images in CI/CDOnly 34%

Step 1: Choose Secure Base Images

# ❌ Full OS image — 1.2GB, 400+ CVEs typical
FROM ubuntu:24.04

# ❌ Slim but still large — 200MB, 50+ CVEs
FROM node:20

# ✅ Alpine — 5MB, minimal attack surface
FROM node:20-alpine

# ✅ Distroless — no shell, no package manager, minimal CVEs
FROM gcr.io/distroless/nodejs20-debian12

# ✅ Scratch — literally empty, for static Go/Rust binaries
FROM scratch

Image Size vs. CVE Comparison

Base ImageSizeTypical CVEs
ubuntu:24.0478MB30-50
node:201.1GB100-200
node:20-slim200MB20-40
node:20-alpine180MB5-15
distroless/nodejs20130MB0-5

Step 2: Write Secure Dockerfiles

❌ Insecure Dockerfile

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]

Problems: runs as root, includes dev dependencies, no .dockerignore, layer caching broken.

✅ Hardened Dockerfile

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app

# Install dependencies first (layer caching)
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force

COPY src/ ./src/

# Production stage
FROM node:20-alpine AS production

# Security: Don't run as root
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app

# Copy only production artifacts
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/src ./src
COPY --chown=appuser:appgroup package.json ./

# Security: Read-only filesystem
USER appuser

# Security: Drop all capabilities
# (Applied at runtime with docker run --cap-drop ALL)

# Health check
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

EXPOSE 3000
CMD ["node", "src/server.js"]

Step 3: Scan Images for Vulnerabilities

# Scan a local image
trivy image myapp:latest

# Scan and fail on HIGH/CRITICAL
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest

# Scan a Dockerfile before building
trivy config Dockerfile

# Output as JSON for CI/CD integration
trivy image --format json --output results.json myapp:latest

CI/CD Integration (GitHub Actions)

- name: Build Docker image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan for vulnerabilities
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    severity: CRITICAL,HIGH
    exit-code: 1
    format: sarif
    output: trivy-results.sarif

- name: Upload scan results
  uses: github/codeql-action/upload-sarif@v2
  with:
    sarif_file: trivy-results.sarif

Step 4: Runtime Security

Docker Compose with Security Options

services:
  app:
    image: myapp:latest
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # Only if needed for port < 1024
    read_only: true
    tmpfs:
      - /tmp:size=64M
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Docker Secrets (Never Use ENV for Secrets)

services:
  app:
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    external: true
  api_key:
    external: true
// Read secret from file, not environment variable
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();

Docker Security Checklist

  • Use minimal base images (Alpine, Distroless, or Scratch)
  • Multi-stage builds — no build tools in production image
  • Run as non-root user (USER directive)
  • .dockerignore excludes .git, node_modules, .env, secrets
  • Pin base image versions (not latest)
  • Scan images in CI/CD pipeline (Trivy, Grype, Snyk)
  • Read-only root filesystem
  • Drop all Linux capabilities, add back only what's needed
  • Resource limits (CPU, memory)
  • No secrets in environment variables — use Docker Secrets
  • Health checks configured
  • Regularly rebuild images to pick up base image patches
Editorial standards

Published by SecureCodeReviews

This article is part of our original AI security and cybersecurity content library. We show publish and update dates, keep company and policy pages public, and update important guidance when material changes affect readers.

Named author: SCRs Team
Published: Mar 22, 2026
Update status: current publication version

Questions or corrections?

Review our editorial standards, learn more about the company, or contact us if a page needs clarification.

AI Security Audit

Planning an AI feature launch or security review?

We assess prompt injection paths, data leakage, tool use, access control, and unsafe AI workflows before they become production problems.

Manual review for agent, prompt, and retrieval attack paths
Actionable remediation guidance for your AI stack
Coverage for LLM apps, MCP integrations, and internal AI tools

Talk to SecureCodeReviews

Get a scoped review path fast

Manual review
Actionable fixes
Fast turnaround
Security-focused

Advertisement