Understanding Docker Compose
One file. A whole local stack.
What docker-compose.yml describes that docker run can't, and why it's still the friendliest way to spin up a real development environment.
The Compose model.
A Compose file describes services, networks and volumes as a single graph that docker compose up brings to life atomically. Each service is a container specification — image, environment, ports, dependencies, mounts. Networks let services see each other by hostname. Volumes persist data outside the container lifecycle. The file is YAML; the engine is Compose v2 (Docker-supported, written in Go) which replaced the original Python tool around 2021.
Service-to-service networking.
When you declare a service named db, Compose creates a default network for the project and gives the service a DNS name of db on that network. Another service in the same file can connect with postgres://db:5432/... — no IP juggling, no docker-network commands. This is one of Compose's biggest wins over hand-rolled docker runsequences: cross-service connectivity becomes "use the service name".
depends_on is shallower than it looks.
depends_on: [db] tells Compose to start the database container before the application container. It does not wait for the database to be ready — Postgres might be starting up for another five seconds after the container exists. The fix is a healthcheck on the dependency plus depends_on: db: condition: service_healthy (Compose v2 syntax). For tooling like wait-for-it.sh, the modern recommendation is to push the wait into the consumer service so the orchestrator stays unaware.
A minimal worked file.
A web app with a database: services:
web:
image: myapp:latest
ports: ["3000:3000"]
environment:
DATABASE_URL: postgres://db:5432/app
depends_on: [db]
db:
image: postgres:16
volumes: [pgdata:/var/lib/postgresql/data]
volumes:
pgdata:
Two services, one named volume, one project-level network created automatically. docker compose up stands the lot up; docker compose down tears it down. The data survives in the named volume across cycles.
Two-service stack
web + db, shared default network, persistent volume
Compose creates the network, starts db first, then web.
services: web, db / volumes: pgdata
= One command stands up the stack
Bind mounts vs named volumes.
Two different mounting strategies. A bind mount ./src:/app/src exposes a host directory directly to the container — useful for dev workflows where you want code edits to show up immediately. A named volume pgdata:/var/lib/... is Docker-managed storage that persists across container rebuilds. Use bind mounts for source code in development; use named volumes for stateful data the host shouldn't see. Mixing them in the same service is fine.
Environment files.
For secrets and per-environment config, every service can read env_file: .env or take individual values via environment:. The standard .env file at the project root is loaded by Compose itself and supports variable interpolation inside the YAML — image: myapp:${TAG} picks up TAG from the env. Don't commit .env; do commit a .env.example showing which keys are expected.
Profiles for optional services.
A modern feature: tag services with profiles: [debug] and they don't start unless invoked with docker compose --profile debug up. Useful for dev-only services (mailcatcher, redis-commander, db UI) you don't want running by default. The Compose file stays one file; the activation is a flag.
What Compose isn't.
Compose is for local development, simple CI environments, and small single-host deployments. It is not Kubernetes — it doesn't do rolling updates, autoscaling, multi-host scheduling, or production-grade health management. Once you outgrow it, tools like Kompose can translate Compose files to K8s manifests as a starting point; but the right time to consider that translation is when you actually need more than one host.