A comprehensive reference for deploying containers securely. Use this as a checklist before every production release — no single measure is sufficient; defense in depth wins.

1. The Hardening Philosophy

Defense in depth means adding independent layers of security so that if one fails, the others still protect you. For containers, that means hardening the image, the runtime, the network, and the orchestrator — not just one layer.

Assume Breach
Attackers will eventually get through. Your goal is to limit blast radius: restrict what a compromised container can read, write, execute, or communicate with. Each hardening step shrinks what an attacker can do after breaking in.
HOST ORCHESTRATOR NETWORK RUNTIME IMAGE

Each ring is an independent security layer — a breach of the inner layer is contained by outer rings.

2. Image Hardening

  • Use minimal base image (distroless / alpine / scratch)
  • Pin base image by digest (FROM image@sha256:…)
  • No secrets in image — use BuildKit --mount=type=secret
  • Scan for vulnerabilities; fail CI on critical findings
  • Generate and attach SBOM (Software Bill of Materials)
  • Sign image with Cosign and enforce policy
  • Rebuild regularly to pick up base image patches

3. Runtime Hardening

  • Run as non-root — USER 10001 (numeric UID)
  • Read-only root filesystem — --read-only
  • Drop all capabilities, add only needed — --cap-drop=ALL --cap-add=NET_BIND_SERVICE
  • Enable --security-opt no-new-privileges
  • Set memory and CPU limits — --memory --cpus
  • Set PID limit — --pids-limit 100 (prevents fork bombs)
  • Use custom seccomp profile if possible
  • Mount sensitive paths as read-only (:ro bind mounts)

4. Network Hardening

  • Use user-defined networks (not default bridge)
  • Don't expose ports unless needed — no -p 0.0.0.0:…
  • Apply network policies to restrict inter-service traffic
  • Enable mTLS between services (or use a service mesh)
  • No --net=host in production unless strictly required

5. Operational Hardening

  • Health checks defined (HEALTHCHECK in Dockerfile)
  • Restart policy set — on-failure:5 (not always)
  • Logs to stdout — captured by log driver, not written to container FS
  • Resource limits prevent noisy-neighbor problem
  • Graceful shutdown — trap SIGTERM, drain connections, exit cleanly
  • Immutable deployments — never patch a running container; redeploy

6. Orchestrator-Level Hardening

  • Pod Security Standards enforced (Restricted profile)
  • Admission control — only signed images from approved registries
  • RBAC configured — least privilege for service accounts
  • Network policies applied per namespace
  • Secrets encrypted at rest in etcd

7. The Quick Audit

The Fully-Hardened docker run Command

Every flag earns its place. Know why each one is there:

docker run \ # start here --user 10001:10001 \ # non-root numeric UID:GID --read-only \ # immutable root FS --tmpfs /tmp:rw,noexec,nosuid,size=64m \ # writable scratch, no exec --cap-drop ALL \ # remove every capability --cap-add NET_BIND_SERVICE \ # re-add only what's needed --security-opt no-new-privileges \ # block privilege escalation --security-opt seccomp=hardened.json \ # custom syscall allowlist --memory 256m --memory-swap 256m \ # hard memory cap, no swap --cpus 0.5 \ # half a CPU core max --pids-limit 100 \ # fork-bomb protection --network app-net \ # user-defined network --restart on-failure:5 \ # retry up to 5 times --health-cmd "curl -f http://localhost/healthz" \ # liveness probe --health-interval 10s --health-retries 3 \ # check frequency myregistry.io/myapp@sha256:abc123... # pinned by digest

Verify with docker inspect

Spot-check key hardening settings
# Check user
docker inspect myapp | jq '.[0].Config.User'
# → "10001:10001"

# Check capabilities
docker inspect myapp | jq '.[0].HostConfig.CapDrop, .[0].HostConfig.CapAdd'
# → ["ALL"]  ["NET_BIND_SERVICE"]

# Check read-only FS
docker inspect myapp | jq '.[0].HostConfig.ReadonlyRootfs'
# → true

# Check resource limits
docker inspect myapp | jq '{mem: .[0].HostConfig.Memory, cpus: .[0].HostConfig.NanoCpus}'
# → {"mem": 268435456, "cpus": 500000000}

# Check PID limit
docker inspect myapp | jq '.[0].HostConfig.PidsLimit'
# → 100

Fully-Hardened Kubernetes PodSpec

hardened-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hardened-app
  annotations:
    # Cosign admission webhook verifies image signature
    policy.sigstore.dev/include: "true"
spec:
  automountServiceAccountToken: false   # no K8s API access unless needed
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    runAsGroup: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault               # kernel syscall filter
  containers:
    - name: app
      image: myregistry.io/myapp@sha256:abc123...
      securityContext:
        allowPrivilegeEscalation: false  # no-new-privileges equivalent
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
          add:  ["NET_BIND_SERVICE"]
      resources:
        requests: { cpu: "100m", memory: "128Mi" }
        limits:   { cpu: "500m", memory: "256Mi" }
      livenessProbe:
        httpGet: { path: /healthz, port: 8080 }
        initialDelaySeconds: 5
        periodSeconds: 10
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: { medium: Memory, sizeLimit: 64Mi }

Knowledge Check

Quiz 1 — Spot the Gap: A team uses a distroless image, runs as UID 10001, and sets --read-only. They expose port 443. Which critical runtime hardening step is most likely still missing?

  • They should use Alpine instead of distroless
  • They haven't dropped capabilities or enabled no-new-privileges
  • They need to rebuild the image more frequently
  • The image should be scanned with Trivy

Quiz 2 — Flag Quiz: What does --pids-limit 100 protect against?

  • Memory exhaustion from large allocations
  • Network flooding (DDoS from within the container)
  • Fork bombs — runaway process creation that exhausts the host PID table
  • Privilege escalation via setuid binaries

Quiz 3 — Risk Assessment: An app container runs in production with --net=host for "simpler networking." What is the primary security risk?

  • The container cannot be restarted automatically
  • Health checks stop working correctly
  • The container shares the host network stack and can bind to any port, sniff all host traffic, and bypass network policies
  • Image signing verification is skipped

Hands-On: Harden an Unhardened Container

Lab: Full Hardening Walkthrough

Starting point — an insecure nginx container:

docker run -d --name insecure-nginx -p 80:80 nginx:latest

Step 1 — Inspect the baseline (spot the problems):

docker inspect insecure-nginx | jq '
  .[0] | {
    user:     .Config.User,
    readonly: .HostConfig.ReadonlyRootfs,
    capdrop:  .HostConfig.CapDrop,
    capadd:   .HostConfig.CapAdd,
    memory:   .HostConfig.Memory,
    pids:     .HostConfig.PidsLimit
  }'
# user: ""  ← running as root!
# readonly: false  ← writable FS
# capdrop: null  ← no caps dropped

Step 2 — Apply every checklist item:

docker network create --driver bridge hardened-net

docker run -d --name hardened-nginx \
  --user 101:101 \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=32m \
  --tmpfs /var/cache/nginx:rw,noexec,nosuid \
  --tmpfs /var/run:rw,noexec,nosuid \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --cap-add CHOWN \
  --security-opt no-new-privileges \
  --memory 128m --memory-swap 128m \
  --cpus 0.25 \
  --pids-limit 50 \
  --network hardened-net \
  --restart on-failure:3 \
  -p 127.0.0.1:8080:80 \
  nginx:alpine

Step 3 — Verify each setting:

# All checks should return expected hardened values
docker inspect hardened-nginx | jq '
  .[0] | {
    user:     .Config.User,
    readonly: .HostConfig.ReadonlyRootfs,
    capdrop:  .HostConfig.CapDrop,
    capadd:   .HostConfig.CapAdd,
    memory:   .HostConfig.Memory,
    pids:     .HostConfig.PidsLimit,
    newnoprivs: .HostConfig.SecurityOpt
  }'

Step 4 — Confirm it's still serving traffic:

curl http://127.0.0.1:8080/
# → Welcome to nginx!

What you proved: Every hardening flag is in place, verified by docker inspect, and the app still works. That's the goal — security without breaking functionality.

Industry Standards & Regulated Environments

🏦 CIS Docker Benchmark, NIST 800-190 & Regulated Industries

CIS Docker Benchmark (Center for Internet Security) is the de-facto audit standard. It maps directly to the checklist above, with scored and unscored checks. Run it with:

docker run --rm -it \
  --net host --pid host --userns host --cap-add audit_control \
  -v /var/lib:/var/lib -v /var/run/docker.sock:/var/run/docker.sock \
  docker/docker-bench-security

NIST SP 800-190 (Application Container Security Guide) provides a risk-based framework covering image risks, registry risks, orchestrator risks, container runtime risks, and host OS risks — aligned with the five rings above.

Banking (PCI-DSS): Requires encrypted secrets, no shared credentials, audit logging of all privileged operations, and network segmentation — all addressed by this checklist.

Healthcare (HIPAA): PHI containers must enforce access controls, encrypt data in transit (mTLS) and at rest (encrypted secrets), and maintain audit trails. Immutable deployments + SBOM are increasingly auditor requirements.

Both frameworks expect you to demonstrate compliance via automated policy enforcement (admission controllers, OPA/Gatekeeper) — not manual reviews.

Key Takeaways

  • Defense in depth — each layer (Image → Runtime → Network → Orchestrator → Host) is independent; failing one doesn't mean all fail
  • Assume breach — harden for damage limitation, not just prevention
  • Non-root + read-only FS + drop capabilities are the highest-impact runtime steps — do these first
  • Pin by digest — tags are mutable; digests guarantee exactly what you tested
  • Automate enforcement — admission controllers beat documentation; policies are enforced, docs get stale
  • CIS Benchmark + NIST 800-190 give you auditable, industry-recognised checklists for regulated environments
  • Verify, don't assume — always run docker inspect to confirm flags applied as expected