Docker Security: Container Scanning, Image Hardening & Runtime Protection
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.
| Finding | Stat |
|---|---|
| Images with critical CVEs | 73% |
| Images running as root | 62% |
| Images with unnecessary packages | 89% |
| Containers with writable root filesystem | 54% |
| Teams scanning images in CI/CD | Only 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 Image | Size | Typical CVEs |
|---|---|---|
| ubuntu:24.04 | 78MB | 30-50 |
| node:20 | 1.1GB | 100-200 |
| node:20-slim | 200MB | 20-40 |
| node:20-alpine | 180MB | 5-15 |
| distroless/nodejs20 | 130MB | 0-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
Using Trivy (Recommended)
# 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)
-
.dockerignoreexcludes .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
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Cloud Security Guide: AWS, Azure & GCP Misconfigurations 2025
Master cloud security with comprehensive guides on S3 bucket security, IAM policies, secrets management, and real breach case studies.
Cloud Security in 2025: Comprehensive Guide for AWS, Azure & GCP
Deep-dive into cloud security best practices across all three major providers. Covers IAM, network security, data encryption, compliance, and real-world misconfigurations that led to breaches.
Multi-Cloud Security Strategy: Unified Controls for AWS, Azure & GCP
87% of enterprises use multi-cloud. This guide provides a unified security strategy — identity federation, network segmentation, CSPM, centralized logging, and consistent policy enforcement across AWS, Azure, and GCP.