Contents

Docker Security Hardening: Production Checklist

Context

Most Docker security incidents aren’t hackers discovering 0days—they’re you forgot to disable privileged mode, ran as root, or exposed unnecessary ports.

This is a practical checklist, not theory. Straight to what to check.

Basic Configuration Checks

1. Don’t Run Containers as Root

# ❌ Wrong: running as root inside container
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

# ✅ Correct: create non-root user
FROM node:20
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
# Same in Kubernetes
securityContext:
  runAsNonRoot: true
  runAsUser: 1000

2. Read-Only Filesystem

# docker-compose.yml
services:
  app:
    read_only: true
    # Use tmpfs for places that need writes
    tmpfs:
      - /tmp
      - /var/run

Container filesystem is read-only—malicious code can’t write.

3. Limit Capabilities

Linux capabilities are root privileges finely divided. Containers don’t need CAP_SYS_ADMIN.

# docker-compose.yml
services:
  app:
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # only add what you need
# Check current container capabilities
docker inspect container_name | grep -i cap

Image Security

4. Use distroless or alpine

# ❌ Don't use ubuntu/debian (200+MB, larger attack surface)
FROM ubuntu:22.04

# ✅ Use distroless (minimal, only runtime needed)
FROM gcr.io/distroless/static-debian12

# ✅ Or alpine (lightweight, only 5MB)
FROM alpine:3.19

5. Don’t Cache Dependencies in Build

# ❌ Wrong: COPY everything then install deps
COPY . .
RUN npm install

# ✅ Correct: package.json first, install deps, then code
COPY package.json package-lock.json* ./
RUN npm ci --only=production
COPY . .

Benefits:

  • Fail fast on dependency issues
  • Better Docker build cache utilization
  • Smaller image layers

6. Set Health Check

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]

Network Isolation

7. Network Minimization

services:
  app:
    networks:
      - backend-network
  
  db:
    networks:
      - backend-network
    # DB doesn't need external exposure

networks:
  backend-network:
    driver: bridge

App only exposes API port, database fully internal.

8. Don’t Use Host Network Mode

# ❌ Dangerous: container uses host network directly
network_mode: "host"

# ✅ Correct: default bridge, port mapping when needed
services:
  app:
    ports:
      - "8080:3000"

Host mode bypasses Docker’s network isolation—container gets direct access to host network.

Secret Management

9. Don’t Store Secrets in Environment Variables

# ❌ Dangerous: secrets in environment variables
environment:
  DATABASE_PASSWORD: "supersecret"

# ✅ Correct: use Docker secrets (Swarm mode)
secrets:
  db_password:
    file: ./db_password.txt

services:
  app:
    secrets:
      - source: db_password
        target: db_password
        mode: 0400

# ✅ Or use external secret management (Vault, AWS Secrets Manager)
# Kubernetes uses external secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
spec:
  secretStoreRef:
    name: vault-backend
  target:
    name: app-secrets
  data:
    - secretKey: db-password
      remoteRef:
        key: prod/database/password

Runtime Security

10. Use AppArmor / seccomp to Limit Syscalls

# Default seccomp profile already blocks 44 dangerous syscalls
# But some scenarios need stricter controls

# View default seccomp
docker run --rm \
  alpine \
  cat /proc/sys/kernel/seccomp/action_on_sigfault

Production recommends non-privileged containers:

security_opt:
  - no-new-privileges:true

11. Resource Limits

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

Prevents containers from exhausting host resources.

CI/CD Security Scanning

12. Trivy for Image Vulnerability Scanning

# .github/workflows/ci.yml
- name: Scan image for vulnerabilities
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.IMAGE_TAG }}
    format: 'sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'  # fail on vulnerabilities
# Run locally too
trivy image your-image:tag

# Scan known CVEs
trivy image -- vuln-type=os,library your-image:tag

13. Don’t Use :latest

# ❌ :latest means you don't know the actual version
image: myapp:latest

# ✅ Use deterministic version
image: myapp:v2.3.1
image: myapp@sha256:abc123...

Summary Checklist

Production Docker must-dos:

□ Run as non-root user
□ Read-only filesystem
□ Minimize capabilities
□ distroless/alpine base image
□ health check configured
□ network isolation
□ no host mode
□ secrets not in env vars
□ resource limits
□ CI/CD vulnerability scanning
□ fixed image versions

Run through this list—prevents most Docker security incidents.