Cloud Security
docker
containers
trivy
security scanning
+3 more

Docker Security: Container Scanning, Image Hardening & Runtime Protection

SCRs Team
March 22, 2026
15 min read
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

Advertisement