Compose Best Practices
Compose is the best tool for a fast, reproducible local development loop, and a poor tool for production orchestration. These practices keep it in its lane: reliable for dev and CI, honest about where Kubernetes takes over.
How to Use This List
Read each group and apply the bullets to your compose.yaml today.
Treat the practices as defaults, not laws. The theme is consistency across teammates and a clean boundary between dev tooling and real deployment.
A - File Structure and Versioning
- Name the file
compose.yaml. It is the current canonical name. Drop the obsolete top-levelversion:key, which the Compose Specification no longer uses. - Keep the base file environment-agnostic. Put shared topology in
compose.yamland local tweaks incompose.override.yaml, which Compose loads automatically. - Commit the Compose files. They are the contract that lets every teammate run the same stack with one command.
- Verify merges with
docker compose config. Before trusting a multi-file setup, print the resolved configuration and read what actually runs. - Use profiles for optional services. Tag debuggers, admin UIs, and seed jobs with
profilesso a plainupstays lean.
B - Images and Builds
- Pin image tags. Use
postgres:17andredis:7, notlatest, so everyone gets identical versions and CI stays deterministic. - Use multi-stage Dockerfiles. Share a base, then split a
devtarget with reload tooling from a lean runtime target, and select withbuild.target. - Rebuild explicitly in CI. Run
docker compose up --buildso pipelines never reuse a stale image from a previous run. - Keep build context small. Add a
.dockerignoresonode_modules,.git, and local artifacts do not bloat the build or leak into images. - Do not bake secrets into images. Pass configuration at run time through
environmentorenv_file, neverCOPYa secret into a layer.
C - Networking and Service Discovery
- Address services by name. Use the service name as the hostname, for example
dborcache, and let Docker DNS resolve it. Never hardcode container IPs. - Publish ports only for host access. App-to-app traffic flows over the internal network already; expose
5432or6379only when a host tool needs them. - Avoid host-port collisions. If a database runs on your host, remap the host side, for example
"5433:5432", rather than fighting for the default port. - Keep networking flat and simple. Compose is single-host. Do not try to model production network policy here; that belongs to Kubernetes.
D - State, Data, and Lifecycle
- Use named volumes for stateful services. They persist across
upanddown, anddocker compose down --volumesgives you a clean reset when you need one. - Use bind mounts for source in dev only. Mount your working tree for live reload, and shadow
node_moduleswith an anonymous volume so container dependencies survive. - Gate startup with healthchecks. Pair
depends_onwithcondition: service_healthyso apps wait for databases instead of racing them. - Use
--waitin automation.docker compose up --waitblocks until services are healthy, which is exactly what CI needs before running tests. - Run migrations as one-off tasks. Use
docker compose run --rm app <migrate>after the database is healthy, reusing the app image and network.
E - Keeping Dev and Prod Honest
- Do not run Compose as production orchestration. It has no cross-host scheduling, self-healing, or rolling updates. That is Kubernetes territory.
- Model production separately. Maintain manifests or a Helm chart with probes, resource requests, and security context rather than converting the Compose file verbatim.
- Remember containerd runs pods, not Docker. In a cluster, containerd via the CRI is the runtime. Docker Engine is for build, dev, and Compose only.
- Treat converter output as a draft. Tools that emit manifests omit probes, resources, ingress, and security defaults. Harden before shipping.
When You Are Done
Your base file should be environment-agnostic, images pinned, services named not IP-addressed, and startup gated by healthchecks.
Optional tooling should sit behind profiles, state should live in named volumes, and production should be modeled in its own manifests.
If a plain docker compose up gives a new teammate a working stack in one command, and nobody mistakes it for production, you have it right.
FAQs
Should I ever deploy with Compose? For a throwaway single-server demo, maybe. For anything needing resilience, scale, or zero-downtime updates, use Kubernetes.
How do I share settings across environments? Keep a neutral base and layer override or environment-specific files, verifying the result with docker compose config.
Why avoid latest tags? They drift silently between machines and over time, breaking reproducibility. Pin explicit versions.
How do I stop the app racing the database? Add a healthcheck to the database and condition: service_healthy to the app's depends_on.
Do I need published ports for services to talk? No. Internal service-name networking handles that. Ports are only for host access.
Is docker compose the same as docker-compose? No. The hyphenated form is the legacy v1 tool. Use Compose v2, invoked as docker compose.
Related
- The Compose Model: Declaring a Local Stack
- Compose Basics
- Compose for Local Dev
- Profiles & Overrides
- Compose vs Kubernetes Gap
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).