Docker Security Best Practices for Production
Harden Docker images and runtimes: non-root users, minimal bases, secret handling, capability drops, and image signing - without making developers miserable.
What you'll learn
- ✓Root vs non-root containers
- ✓Minimal and pinned base images
- ✓Secret handling at build and run
- ✓Capability drops and read-only filesystems
- ✓Image signing and SBOMs
Prerequisites
- •Familiar with terminals and YAML
What and Why
A container is just a process with namespaces and cgroups. If that process runs as root with all Linux capabilities, a small CVE in your app can become a host compromise. Docker security is the practice of shrinking the blast radius so an exploit gets you very little.
This matters because the supply chain is now a primary attack vector. Compromised base images, leaky secrets in layers, and over-privileged runtimes are real outages and real breaches, not theoretical risks.
Mental Model
Build time: Run time:
- minimal base - non-root user
- pinned digests - read-only rootfs
- SBOM + scan - drop capabilities
- no secrets in layers - seccomp / AppArmor
- signed images - resource limits
Image registry -- signature & SBOM verified --> cluster admission Each layer adds friction for an attacker. None alone is enough. The combination is what stops the chain.
Hands-on Example
A hardened Dockerfile for a small Go service:
# syntax=docker/dockerfile:1.7
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Run it with hardened flags:
docker run -d \
--read-only \
--tmpfs /tmp:rw,size=64m \
--cap-drop=ALL \
--security-opt no-new-privileges \
--pids-limit=200 \
--memory=512m --cpus=1 \
-p 8080:8080 \
app:1.0.0
For secrets at build time, never bake them into layers. Use BuildKit secrets:
DOCKER_BUILDKIT=1 docker build \
--secret id=npmrc,src=$HOME/.npmrc \
-t app .
In the Dockerfile:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
The secret is mounted only for that step and never lands in the image.
At runtime, inject secrets via environment from a secret manager (AWS Secrets Manager, Vault) or Kubernetes Secrets - not via --env from shell history.
Common Pitfalls
- Running as root.
USER 0is the default for many bases. Switch to a non-root user and verify withdocker exec id. COPY .envOnce in a layer, always in a layer, even if a laterRUN rmdeletes it. Image history reveals it. Use.dockerignoreand runtime secrets.- Using
:latest. Reproducibility and security both suffer. Pin to digests (image@sha256:...) and update via automation. - Leaving the package manager in the runtime image.
apt,apk,npm, and shells are valuable to attackers. Distroless removes them. - Privileged containers.
--privilegeddisables nearly all isolation. Almost always the wrong answer; use specific capabilities instead.
Production Tips
- Scan images in CI with Trivy or Grype. Fail the build on critical CVEs and known malicious packages. Re-scan on a schedule even when the image does not change.
- Generate and publish an SBOM (
syft,docker sbom) per image. You will thank yourself the next time a Log4Shell happens. - Sign images with cosign and require signatures at admission (Kyverno, Sigstore policy controller, ECR signing). Unsigned images do not run.
- Use rootless Docker or Podman on developer machines. Same UX, lower blast radius.
- In Kubernetes, enforce a Pod Security Standard of
restrictedand refuse hostPath, privileged, and runAsRoot via admission policies.
Wrap-up
Container security is a stack of small disciplines: minimal base, non-root user, pinned digests, no secrets in layers, dropped capabilities, read-only rootfs, signed images, and ongoing scans. None is hard on its own and the combination dramatically reduces what an attacker can do with a 0-day in your app. Bake these defaults into your templates and CI so doing the right thing is also the easiest thing.
Related articles
- Docker Docker Healthchecks and Restart Policies Explained
Healthchecks tell Docker if a container is alive. Restart policies tell it what to do when it is not. Together they keep your services running.
- Docker Docker Image Layer Caching Strategies for Faster Builds
Learn how Docker's layer cache really works and the ordering tricks that turn a 5-minute build into a 20-second one without sacrificing correctness.
- Docker Docker Networking: Bridge, Host, and Overlay Networks Explained
A clear guide to Docker's three most common network drivers, when to pick each one, and how packets actually flow between containers in real deployments.
- Docker Docker Volumes vs Bind Mounts Explained
The practical differences between Docker volumes and bind mounts, when to pick each, and how to avoid the permission and performance traps.