What Docker Compose Is and When to Reach for It
Docker Compose turns a YAML file into a multi-container application. Where it shines, where it stops being enough, and how it fits next to Kubernetes.
Published on • October 15, 2026
AI Assistant

Real applications aren’t one container. They’re a web process, a database, a cache, maybe a queue and a worker. Running those with a wall of docker run commands — each needing the right network, the right volume, the right port mapping, in the right order — is where Docker Compose comes in. You describe the whole stack in one YAML file; one command brings it up.
What Compose actually is
Docker Compose is a tool for defining and running multi-container applications. As Docker’s docs put it, it “simplifies the control of your entire application stack, making it easy to manage services, networks, and volumes in a single YAML configuration file. Then, with a single command, you create and start all the services from your configuration file.”
That single file — compose.yaml — declares:
- services: each one an image (or a build from a Dockerfile) plus its runtime config — environment, ports, volumes, dependencies, resource limits.
- networks: the virtual networks services use to reach each other by name. Compose creates a default one for the project.
- volumes: named storage that outlives containers, mounted into services that need persistence.
Services discover each other by service name on the Compose network, so web can just connect to db:5432 — no hardcoded IPs.
The lifecycle commands cover the whole app, not just one container: start, stop, and rebuild services; view the status of running services; stream logs; and run a one-off command on a service. In practice that’s docker compose up -d, docker compose ps, docker compose logs -f, docker compose down, docker compose exec web sh.
A minimal but real example
# compose.yaml
services:
web:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://app:app@db:5432/app
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 5
volumes:
dbdata:
docker compose up -d builds the web image if needed, creates the network, starts Postgres, waits for its healthcheck to pass (because of condition: service_healthy), then starts the web service. docker compose down tears the whole thing down; add -v to also drop the named volumes.
Where Compose shines
Local development. The onboarding pitch writes itself: clone the repo, run docker compose up, and the whole environment is there — same Postgres version as production, same sidecar services, no “works on my machine.” The volumes: - .:/app bind mount plus a reload-capable dev server gives you live code editing inside the containerized stack.
CI workflows and testing. Compose works in all environments — production, staging, development, testing, as well as CI. A CI job can boot the full stack, run integration tests against http://web:8000, and tear it down. Ephemeral, reproducible, and identical to what developers run.
Single-host deployments. Plenty of real production deployments are one beefy VM running Compose: a handful of services, a reverse proxy, restart policies, and a backup cron. If you don’t need to span multiple machines, Compose is operationally simple and hard to beat.
Demos, scripts, and one-off tooling. A compose file in the repo documents an application’s topology better than a README paragraph ever will.
Where it stops being enough
Compose is fundamentally a single-host orchestrator. It manages containers on one Docker Engine, and that boundary shows up in several places:
- No cross-host scheduling. If the node dies, everything on it dies. There’s no scheduler to relocate services to another machine, no replica spreading, no cluster state.
- Manual HA. Scaling a service with
--scale web=3runs more replicas on the same host behind Compose’s own load balancing. Real high availability across failure domains isn’t in scope. - Limited rolling updates. There’s no native rolling-replacement with health-gated rollouts and automatic rollback the way a cluster orchestrator provides. Deployments are closer to “recreate” than “surge.”
- Basic health-based recovery.
restart: unless-stoppedbrings a dead container back on the same host, but there’s no control-plane supervision across nodes. - No built-in secret distribution, service mesh, or autoscaling. You bolt these on yourself — fine for a handful of services, unwieldy at fleet scale.
Compose next to Kubernetes
Kubernetes picks up exactly where Compose leaves off: multi-node scheduling, self-healing with liveness and readiness probes, rolling and rollback deployments, horizontal autoscaling, and a much richer networking and secret model. The trade-off is a big step up in operational complexity — control planes, etcd, RBAC, CNI plugins — plus a more verbose declarative model (Deployments, Services, Ingresses, PVCs) for what Compose expresses in twenty lines.
A sane progression many teams follow: Compose for local dev and single-host staging or production; Kubernetes when you need more than one node, real rolling deploys, or an org-wide platform. The two coexist well — the same images and environment variables move from a compose file on a laptop to manifests in a cluster, and Compose files can even be converted to Kubernetes manifests as a starting point. Notably, Compose doesn’t retire at that boundary: developers still use it locally even when production is entirely on Kubernetes.
Practical gotchas
depends_onwithoutcondition: service_healthyonly waits for start, not readiness. Your web app will race Postgres on first boot unless the database has a healthcheck and you gate on it.- Project name collisions. Compose scopes resources by project name (derived from the directory name by default). Two checkouts with the same directory name will fight over container and network names. Set
name:in the compose file or use-p/COMPOSE_PROJECT_NAME. .envhandling surprises. Compose reads.envfrom the project directory for variable interpolation. Don’t confuse interpolation-time variables (.env) with runtimeenvironment:values — and keep real secrets out of the file you commit.down -vdestroys data. Named volumes survivedocker compose down, which is usually what you want in dev — but a careless-vin a script wipes your database.- Bind mounts behave differently across OSes. File path semantics and file-watching differ between Linux, macOS, and Windows hosts; a bind mount that hot-reloads on Linux may not on Docker Desktop without the right cache settings.
- Rebuilds don’t happen by default. After changing a Dockerfile,
docker compose up -dwon’t rebuild unless you pass--build. Tagging images:latesthides this problem until it bites you mid-demo. - Resource limits are opt-in. Unlike Kubernetes, Compose won’t enforce CPU/memory defaults; one runaway service can starve the host. Declare
deploy.resourceslimits for anything long-lived.