Skip to content
C Codeloom
Docker

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.

·4 min read · By Codeloom
Intermediate 10 min read

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
Defense in depth for containers

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 0 is the default for many bases. Switch to a non-root user and verify with docker exec id.
  • COPY .env Once in a layer, always in a layer, even if a later RUN rm deletes it. Image history reveals it. Use .dockerignore and 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. --privileged disables 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 restricted and 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.