A real app is never just one container — it's your app plus a database plus Redis plus maybe a queue, each needing the right versions, network wiring, and startup order. Docker Compose declares that whole stack in one file, so 'set up my dev environment' becomes one command instead of a page of manual steps.
3 min read
A single docker run command is fine for one container. But a realistic backend needs a Postgres database, a Redis cache, and maybe a background worker — each its own container, each needing to find and talk to the others by name, each needing to start in a sane order (the app shouldn't come up before the database is accepting connections).
Doing this by hand means remembering a growing list of docker run commands, custom network setup, and volume mounts — different for every developer, easy to get subtly wrong, and nowhere written down as a single source of truth.
Docker Compose declares every container your app needs, how they're networked, and what data they persist, in one docker-compose.yml:
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7-alpine
volumes:
db_data:One command brings the entire stack up:
docker compose upCompose creates a private network for these services automatically, and — this is the part that removes a whole category of config headaches — each service can reach the others by service name. The app's DATABASE_URL says db, not an IP address, because Compose's built-in DNS resolves db to whichever container is currently running that service.
depends_on and the startup-order trapdepends_on controls start order, not readiness. depends_on: db alone only guarantees the db container has started — not that Postgres inside it has finished initializing and is accepting connections. An app that connects to the database immediately on boot can crash on a cold docker compose up even though depends_on was set correctly.
The fix is the condition: service_healthy shown above, paired with a healthcheck on the dependency. Compose then waits for the database's own health check to pass — not just for the container process to exist — before starting the app. Without a health check, depends_on is only ever "started roughly first," never "actually ready."
Containers are ephemeral by default — stop and remove the db container, and every row in it is gone. The db_data volume declared above is a named volume: Docker manages its storage outside the container's own filesystem, so it survives docker compose down (though not docker compose down -v, which explicitly deletes volumes too). This is what lets a developer restart their whole stack for a code change without re-seeding the database every time.
Compose supports layering files — a base docker-compose.yml plus a docker-compose.override.yml (loaded automatically) or an explicit -f for CI/staging variants:
# Local dev — uses docker-compose.yml + docker-compose.override.yml automatically
docker compose up
# CI — explicit files only, no local override leaking in
docker compose -f docker-compose.yml -f docker-compose.ci.yml upThis keeps one canonical stack definition while letting local-only conveniences (bind-mounting source code for hot reload, exposing extra debug ports) live in a file that's gitignored or clearly separated from what CI actually runs.
Compose is a local development and single-host tool — it's how a developer gets the full stack running on their laptop in one command, and it's genuinely good at that. It is not a production orchestrator: it has no rolling updates, no multi-host scheduling, no built-in auto-scaling. Production deployment uses ECS, Kubernetes, or an equivalent (covered later in this domain) — Compose's job ends at "works identically on every developer's machine."
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does Docker Compose actually add on top of writing individual 'docker run' commands?
2. Why can 'depends_on' alone still let an app crash on a cold 'docker compose up'?
3. In the example docker-compose.yml, how does the app service find the database?
4. What is Docker Compose NOT designed to do?
Docker & Containers