Real applications are rarely one container. A typical web stack is an app server, a database, and a cache - and starting each with a long docker run command, on the right network, in the right order, gets painful fast. Docker Compose solves this: you describe the whole stack in one YAML file and manage it with single commands.
Create a file named compose.yaml (the older name docker-compose.yml also works):
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:s3cret@db:5432/shop
REDIS_URL: redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: s3cret
POSTGRES_DB: shop
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
pgdata:Then bring everything up:
docker compose up -dCompose builds the api image, pulls postgres and redis, creates a dedicated network, starts all three containers, and wires them together. Because every service joins the same network, the api reaches the database simply at hostname db and the cache at cache - service names become DNS names automatically.
docker compose up -d # start (and create) everything
docker compose ps # status of this project only
docker compose logs -f api # follow logs for one service
docker compose exec api sh # shell into a running service
docker compose restart api # restart one service
docker compose down # stop and remove containers + network
docker compose down -v # ...and delete named volumes tooNote that down preserves named volumes by default, so your database survives. Add -v only when you genuinely want a clean slate.
When the Dockerfile or source changes, rebuild and restart in one step:
docker compose up -d --buildCompose is smart about state: it only recreates containers whose configuration or image actually changed.
host:container mapping as docker run -p../src:/app/src for live-reload development. db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
api:
depends_on:
db:
condition: service_healthyNow the api container starts only after Postgres actually accepts connections - the fix for the classic "connection refused on startup" race.
Compose namespaces everything (containers, network, volumes) with a project name derived from the folder. Two different projects on one machine never collide, and docker compose down cleans up only its own resources.
docker run commands with one declarative YAML file.up -d, logs -f, exec, and down cover the daily workflow.depends_on orders startup; healthchecks with service_healthy guarantee readiness.down and up.Next up: Dockerizing a Node.js Application, a full end-to-end walkthrough putting Dockerfile, networking, and Compose together.