Docs
/
Docker Kubernetes
Chapter 8

08 — Docker Security

Security Best Practices

1. Run as Non-Root

# ❌ Default: runs as root
FROM node:20-alpine
COPY . /app
CMD ["node", "app.js"]

# ✅ Run as non-root user
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "app.js"]

2. Use Minimal Base Images

node:20        → ~1 GB   (Debian, many packages)
node:20-slim   → ~250 MB (minimal Debian)
node:20-alpine → ~180 MB (Alpine Linux, minimal)
distroless     → ~30 MB  (no shell, no package manager)
# Most secure: distroless (no shell to exploit)
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app/dist /app
CMD ["app/server.js"]

3. Read-Only Filesystem

docker run --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  my-app

4. No Secrets in Images

# ❌ Secret baked into image (visible in layer history)
ENV API_KEY=sk_live_abc123

# ✅ Pass at runtime
# docker run -e API_KEY=sk_live_abc123 my-app

# ✅ Use Docker secrets (Swarm) or mount secret files
# docker run -v /host/secrets:/run/secrets:ro my-app

5. Drop Capabilities

# Drop all capabilities, add only what's needed
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE my-app

# No new privileges
docker run --security-opt no-new-privileges my-app

6. Scan for Vulnerabilities

# Scan before deploying
trivy image my-app:latest
docker scout cves my-app:latest

# In CI/CD pipeline
trivy image --exit-code 1 --severity CRITICAL my-app:latest

7. Use Specific Image Tags

# ❌ Mutable tag — can change without notice
FROM node:latest
FROM node:20

# ✅ Pin to specific digest
FROM node:20.11.1-alpine
# or
FROM node@sha256:abc123...

Docker Compose Security

services:
  api:
    image: my-app:1.0.0
    read_only: true
    tmpfs:
      - /tmp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    user: "1000:1000"
    mem_limit: 512m
    cpus: 1.0
    pids_limit: 100

Security Checklist

□ Non-root USER in Dockerfile
□ Alpine or distroless base image
□ Specific version tags (not :latest)
□ No secrets in Dockerfile or image layers
□ .dockerignore excludes .env, .git, node_modules
□ Image scanned for CVEs
□ Read-only filesystem where possible
□ Capabilities dropped
□ Resource limits set (memory, CPU, PIDs)
□ Health checks configured
□ Signed images in production

Key Takeaways

  • Never run as root — always set USER in Dockerfile
  • Use alpine or distroless base images — smaller attack surface
  • Never bake secrets into images — use runtime env vars or secret managers
  • Scan images for vulnerabilities in CI/CD (fail on CRITICAL)
  • Use --read-only, --cap-drop ALL, --security-opt no-new-privileges
  • Set resource limits to prevent container from consuming all host resources