Local Dev Best Practices
This is a working checklist of habits that keep local containerized development fast without letting it drift from what you actually ship. The through-line: if it works only in Compose, it will fail in Kubernetes, so test the production image.
How to Use This List
Treat each group as a review pass on your repo's local setup.
Adopt the whole of Group A first (it is the parity foundation), then work outward as pain points appear.
Every practice is a bold rule followed by the reason, so you can skim the rules and read the reasons where you disagree.
A - Parity and Artifacts
- Ship the same image everywhere. Promote one image digest from CI through staging to prod; do not rebuild per environment, because each rebuild is a fresh chance to diverge.
- Pin base images by digest. A tag like
node:22-slimre-points over time;@sha256:...freezes the exact base so local and CI agree. - Commit your lockfiles.
package-lock.json,go.sum,poetry.lock, and friends make dependency resolution reproducible across machines. - Inject config at run time, never build time. Database URLs, log levels, and flags belong in env, ConfigMaps, and Secrets so one image serves all environments.
- Never bake secrets into images. Layers are cached and shippable; a secret in a layer leaks. Mount secrets at runtime instead.
- Avoid
latest, even locally. It hides which artifact you are running and quietly breaks the parity you are trying to protect.
B - The Dockerfile
- Use multi-stage builds. A
buildstage compiles and a slimruntimestage ships only artifacts, keeping compilers and dev dependencies out of production. - Order layers cheap-to-expensive. Copy lockfiles and install dependencies before copying source, so a code edit does not bust the dependency cache.
- Run as a non-root user. Add
USERin the runtime stage; it matches Pod Security Standards (restricted) and limits blast radius. - Use a
.dockerignore. Exclude.git,node_modules, and local artifacts so the build context stays small and cache stays stable. - Lean on BuildKit cache mounts.
RUN --mount=type=cache,...keeps package caches warm across builds without landing them in a layer. - Set an explicit
# syntax=directive. Opting into the current Dockerfile frontend makes builds predictable across Docker versions.
C - Compose and the Inner Loop
- Bind-mount source, not
node_modules. Shadow the host modules with an anonymous volume so architecture-specific binaries stay correct. - Gate dependencies with healthchecks. Use
depends_onwithcondition: service_healthyso your app waits for the database instead of racing it. - Prefer
docker compose watchfor sync + rebuild. It syncs source for speed and rebuilds on lockfile changes for fidelity, encoding the right trade-off. - Enable polling only when watchers miss events. Virtualized host mounts can drop
inotifyevents; reach for polling as a targeted fix, not a default. - Persist stateful data in named volumes. Keep your local database across runs, and use
down -vdeliberately when you want a clean slate. - Keep dev config in git-ignored env files. An
.env.devfeeds Compose without ever entering the image or the repo.
D - Kubernetes-Aware Local Testing
- Run the production image locally before merge. Build the real
runtimetarget and run it--read-onlyto catch write-path and non-root bugs early. - Keep a local cluster for K8s-only behavior. kind, minikube, or k3d exercise probes, limits, security context, and NetworkPolicy that Compose cannot express.
- Remember containerd runs your pods. Docker builds images; on nodes, containerd via the CRI runs them, so
kind load docker-image(not the Docker daemon) is how the image reaches the cluster. - Set requests, limits, and probes locally. Testing them catches OOM kills and crash-loops that only appear under Kubernetes scheduling.
- Test against a default-deny NetworkPolicy. Verifying required flows locally prevents a service that "worked in Compose" from being blackholed in a locked-down namespace.
E - Security and Supply Chain
- Scan images in the inner loop. Run Trivy or Grype against your build so CVEs surface before code review, not after deploy.
- Pin and verify base provenance. Digest-pinned bases plus signature verification (cosign/Sigstore) keep a poisoned upstream from reaching you.
- Match Pod Security Standards early. Develop against a non-root, read-only, no-added-capabilities posture so restricted namespaces do not reject your pod later.
- Keep
.envand kubeconfig out of git. Local credentials are the easiest secret to leak; ignore them and rotate anything committed by accident.
When You Are Done
You should be able to build the production image, run it locally read-only and non-root, and see it start under a local Kubernetes with probes and limits.
Config should be the only thing that changes between your laptop and prod.
If any step "only works in Compose", stop and fix the image before it fails in the cluster.
FAQs
What is the single most important practice? Promote one image digest end-to-end and inject config at runtime; that alone kills most "works on my machine" failures.
Do I really need a local Kubernetes? For probes, limits, security context, and network policy, yes - Compose cannot reproduce them. For plain inner-loop coding, Compose is enough.
Why not just use latest locally for convenience? It obscures which artifact you are running and breaks parity; the small convenience is not worth the debugging cost.
How do I keep dev fast but still faithful? Use bind mounts or compose watch for iteration, and gate merges on a real production-image run so speed never hides a shipping bug.
Where do security scans fit locally? Run Trivy or Grype during your build so vulnerabilities and misconfigurations appear in the inner loop rather than in production.
Related
- Dev/Prod Parity as a Mental Model
- Local Dev Basics
- Bind Mounts vs Image Rebuild
- Dev Containers
- Hot Reload in Containers
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).