Container Fundamentals Best Practices
This is a working list of container fundamentals that hold up in production, aimed at tech leads setting defaults for a team.
The theme is simple: one concern per container, immutable artifacts, least privilege, and resources declared explicitly.
How to Use This List
Treat these as defaults, not laws - adopt them wholesale, then document each deliberate exception in an ADR.
Group A covers process shape, B covers image hygiene, C covers security posture, D covers resources and health, and E covers the runtime boundary.
Wire the checkable items into CI (linting, scanning, policy) so they hold without relying on memory.
A - Process and Container Shape
- One process (one concern) per container by default. Keep the container to a single foreground process so its lifecycle, logs, and health map cleanly; split extra concerns into sidecars or separate pods.
- Document every multi-process exception. If you must run a supervisor (legacy app, tightly coupled helper), record why in an ADR and prefer a real init that reaps zombies and forwards signals.
- Make your process PID 1 behave. Handle
SIGTERMfor graceful shutdown and use an init shim (--initortini) when your app spawns children, so signals and zombies are handled correctly. - Log to stdout and stderr. Write logs to the standard streams and let the platform collect them; do not manage log files inside an ephemeral container.
- Keep containers stateless. Push durable state to volumes, databases, or object storage so any container can be killed and rescheduled without data loss.
B - Image Hygiene
- Start from minimal, trusted bases. Prefer distroless or slim/alpine images to shrink attack surface and size; fewer packages means fewer CVEs to patch.
- Pin bases by digest, not floating tags. Reference
image@sha256:...(or an immutable semantic tag) so builds are reproducible and cannot silently change under you. - Order Dockerfile layers for cache and size. Copy dependency manifests and install before copying source, and combine related
RUNsteps so the cache survives code-only changes. - Use multi-stage builds. Compile in a build stage and copy only the artifact into a tiny runtime stage, keeping compilers and dev dependencies out of the shipped image.
- Never bake secrets into images. Secrets baked into layers persist in history; inject them at runtime via Secrets or an external manager instead.
- Build multi-arch when you run mixed nodes. Publish an image index for
amd64andarm64so the same tag runs on Graviton and x86 withoutexec format error.
C - Security Posture
- Run as a non-root user. Set a
USERin the image andrunAsNonRoot: truein the pod so a compromise does not start as root inside the container. - Drop all capabilities, add back only what you need. Start from
drop: ["ALL"]and grant specific capabilities explicitly; almost no app needs the default set. - Make the root filesystem read-only. Set
readOnlyRootFilesystem: trueand mount anemptyDirfor the few writable paths, so tampering and drift are blocked. - Disable privilege escalation and apply seccomp. Set
allowPrivilegeEscalation: falseand use theRuntimeDefaultseccomp profile to shrink the syscall attack surface. - Enforce the restricted Pod Security Standard. Label namespaces to enforce
restrictedso insecure pod specs are rejected at admission, not discovered later. - Scan and sign images in CI. Gate releases on a Trivy or Grype scan and sign with cosign so only verified, known-good images deploy.
D - Resources and Health
- Set memory requests and limits. Requests drive scheduling and limits become the cgroup ceiling; a right-sized memory limit prevents one pod from OOMing the node.
- Set CPU requests; use CPU limits cautiously. Requests ensure fair scheduling, but aggressive CPU limits throttle and add tail latency, so limit only when you must cap noisy neighbors.
- Add readiness and liveness probes. Readiness gates traffic until the app is serving; liveness restarts a wedged process - keep them cheap and distinct.
- Add a startup probe for slow starters. Protect slow-booting apps from premature liveness restarts by giving them a startup probe with a generous threshold.
- Size runtimes from cgroup limits, not host topology. Configure JVM heap, Node, and Go thread pools from the container's limits, since
/procoften still reports host CPUs and memory.
E - Runtime and Platform Boundary
- Know the Docker/containerd split. Use Docker for build, dev, and Compose; remember pods on nodes run via containerd and runc over the CRI - dockershim is gone.
- Never rely on Docker socket access in-cluster. Mounting the Docker or containerd socket is a host-takeover risk; use rootless builders (BuildKit, buildah, Kaniko) for in-cluster builds.
- Use a stronger boundary for untrusted code. Containers share the host kernel, so isolate hostile or multi-tenant workloads with a RuntimeClass backed by gVisor or Kata.
- Ship immutable artifacts and roll back by digest. Build once, promote the same digest across environments, and roll back by redeploying the prior digest rather than patching live.
- Default-deny the network. Apply a default-deny NetworkPolicy per namespace and open only the flows you need, so a compromised pod cannot roam freely.
When You Are Done
You should be able to point at CI gates for image scanning, signing, and non-root/restricted policy, not just documentation.
Every running container should have a single clear concern, explicit resources, probes, and an immutable, digest-pinned image.
Any deviation - a multi-process container, a missing limit, an elevated capability - should have a written, reviewed reason.
FAQs
Is one process per container a hard rule? It is a strong default, not dogma; sidecars and rare supervised apps are fine when justified and documented with a real init.
Requests or limits - which matters more? Requests drive scheduling and are almost always worth setting; memory limits protect the node, while CPU limits should be used sparingly to avoid throttling.
Why non-root if the container is isolated? Isolation is not a hard boundary, so non-root plus dropped capabilities limits the damage if the process is compromised or escapes.
Do these apply to Docker Compose dev setups too? Many do (non-root, pinned bases, one concern), but the security and resource controls are enforced by Kubernetes in production, not Compose.
How do I enforce this without nagging? Push checks into CI and admission control - scanning, signing, and the restricted Pod Security Standard - so violations are blocked automatically.
Related
- What a Container Really Is
- Container Basics
- Containers vs Virtual Machines
- OCI Image & Runtime Specs
- Linux Namespaces & cgroups
- Immutable Infrastructure Principle
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).