Networking & Storage Best Practices
A short, opinionated list for wiring container networking and storage the way a production team should.
It spans single-host Docker and multi-node Kubernetes, and it favors defaults that are secure, portable, and easy for the next engineer to reason about.
How to Use This List
Read each group as a checklist for a service you are about to ship or review.
Bold leads are the rule; the sentence after is the reasoning or the concrete action.
Apply the ones that fit your topology - single-host Compose or a Kubernetes cluster - and treat the security items as non-negotiable.
A - Networking Fundamentals
Use user-defined networks, never the default bridge. The default bridge has no DNS, so create a network and let containers resolve each other by name.
Talk by name, not by IP. Container and Pod IPs are ephemeral; hardcoded IPs break on the first restart or reschedule.
Publish the minimum surface. Only publish ports that truly need external reach, and bind to 127.0.0.1 when a port is for local tooling only.
Segment tiers onto separate networks. Put the database on a backend network the public tier cannot touch, so a compromised frontend cannot reach it directly.
Do not reach for host networking by default. host mode removes isolation; use it only for a trusted, high-throughput single container that needs raw host ports.
B - Kubernetes Networking
Front Pods with a Service, never target Pod IPs. A Service gives a stable virtual IP and cluster DNS name over a changing set of Pods.
Use Ingress or Gateway API for external HTTP. These provide routing, TLS termination, and host/path rules that raw NodePorts cannot.
Apply a default-deny NetworkPolicy per namespace. Start closed, then allow only the flows each workload needs, so lateral movement is blocked by default.
Verify Gateway API controller support before adopting it. Gateway API is GA, but capabilities differ by controller and cloud, so confirm your ingress implementation covers what you need.
Remember containerd runs the Pods. Nodes use containerd through the CRI; Docker Engine is for build and local dev, not the in-cluster runtime.
C - Storage and Persistence
Keep durable state in named volumes or PersistentVolumes. The container's writable layer is ephemeral and disappears on removal.
Design workloads to be stateless where possible. Push real state into managed volumes or external datastores so a lost container costs nothing.
Prefer named volumes over bind mounts in production. Volumes are engine-managed and portable; bind mounts couple you to a specific host path and its permissions.
Use CSI-backed PersistentVolumeClaims in clusters. A bind mount or hostPath breaks when a Pod reschedules to another node, and hostPath is a security risk.
Mount config and secrets read-only. Append :ro or readonly so a container cannot rewrite the files it was handed.
D - Permissions and Security
Run as a fixed non-root numeric UID. Bake the UID into the image and give it ownership of its data path, so volume permissions are predictable.
Never paper over permissions with chmod 777. It creates world-writable data and hides the real UID mismatch you should fix.
Use fsGroup for shared volume ownership in Kubernetes. It sets group ownership so a non-root Pod can write without a runtime chown.
Enforce Pod Security Standards restricted. Require runAsNonRoot, drop capabilities, and forbid privilege escalation on every workload that can meet it.
Prefer tmpfs for secrets and scratch. In-memory mounts never hit disk and vanish when the container stops.
E - Operability and Documentation
Document required ports and mount paths in the README. The next engineer should not reverse-engineer which volumes and ports a service needs to run.
Name volumes and networks explicitly. Anonymous volumes accumulate as untagged clutter and are easy to delete by accident.
Pin image tags or digests for stateful services. A surprise database image bump can migrate or corrupt on-disk formats.
Add healthchecks and readiness gates. Use condition: service_healthy in Compose and readiness probes in Kubernetes so dependents wait for real availability.
Test data survival explicitly. Remove and recreate the container in staging to prove the volume, not the writable layer, holds your state.
When You Are Done
You should be able to point at exactly where each service's state lives and which ports it opens.
Every workload should run as a non-root UID with predictable volume ownership and no chmod 777 anywhere.
Networking should be name-based, segmented, and default-deny, with external reach flowing through Services and Ingress or Gateway API rather than raw Pod IPs.
FAQs
Named volume or bind mount for production data? Named volumes. They are portable, engine-managed, and avoid host-path coupling; bind mounts are for local development.
How do containers reach each other without published ports?
On a shared user-defined network or within a Pod, they talk directly by name or localhost; publishing is only for external reach.
What is the single most impactful security default?
Run as a non-root numeric UID and enforce Pod Security Standards restricted.
Why not use hostPath volumes in Kubernetes?
They break on reschedule to another node and expose host filesystem paths, which is a real security risk.
Should I document ports and mounts even for small services? Yes. A short README of required ports and mount paths saves hours of reverse-engineering later.
Related
- How Containers Get Networking and Storage
- Docker Networking Basics
- User-Defined Networks
- Volumes & Bind Mounts
- Storage Permissions Pitfalls
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).