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.
Each ring is an independent security layer — a breach of the inner layer is contained by outer rings.
2. Image Hardening
Image Layer- 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
Runtime Layer- 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 (
:robind mounts)
4. Network Hardening
Network Layer- 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=hostin production unless strictly required
5. Operational Hardening
Ops Layer- Health checks defined (
HEALTHCHECKin Dockerfile) - Restart policy set —
on-failure:5(notalways) - 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
Orchestrator Layer- 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:
Verify with docker inspect
# 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
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?
Quiz 2 — Flag Quiz: What does --pids-limit 100 protect against?
Quiz 3 — Risk Assessment: An app container runs in production with --net=host for "simpler networking." What is the primary security risk?
Hands-On: Harden an Unhardened Container
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 (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 inspectto confirm flags applied as expected