Docker Compose
Real applications are rarely a single container. A web app needs a database, maybe a cache, maybe a message broker. Docker Compose lets you describe that whole stack in one declarative docker-compose.yml file and manage it with a single command. It replaces a pile of ad-hoc docker run flags with a versioned, reviewable specification.
The file structure
A Compose file has a few top-level keys. The most important is services — each service becomes one or more containers. volumes and networks declare shared resources the services reference.
services: # the containers that make up the app
web: { }
db: { }
volumes: # named, persistent storage
pgdata:
networks: # custom networks (an implicit default is created too)
backend:
A web + database example
Here is a complete stack: a Node web service that talks to a Postgres database.
services:
web:
build: . # build from the local Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
db:
condition: service_healthy
networks:
- backend
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
networks:
- backend
volumes:
pgdata:
networks:
backend:
Service-to-service networking
Compose creates a network where each service is reachable by its service name as a DNS hostname. That is why the web service connects to db:5432 rather than an IP — Compose resolves db automatically. Only ports listed under ports are published to the host; everything else stays internal to the network.
depends_on and startup order
depends_on controls start ordering. On its own it only waits for the container to start, not for the app inside to be ready. Pairing it with condition: service_healthy (backed by a healthcheck) makes web wait until Postgres actually accepts connections.
depends_ondoes not retry your application’s connection logic. Always make services resilient to a dependency that is briefly unavailable, even with health checks in place.
Managing the stack
A few commands cover the entire lifecycle:
docker compose up -d # build (if needed) and start everything detached
docker compose ps # list the stack's containers
docker compose logs -f web # follow logs for one service
docker compose down # stop and remove containers + networks
docker compose down -v # also remove named volumes (deletes data!)
Output of docker compose up -d:
[+] Running 3/3
✔ Network app_backend Created
✔ Container app-db-1 Healthy
✔ Container app-web-1 Started
To rebuild after changing your Dockerfile or source, add --build:
docker compose up -d --build
docker compose down -vpermanently deletes named volumes. Omit-vto stop the stack while preserving your database data.
Best Practices
- Keep
docker-compose.ymlin version control as the single source of truth for the stack. - Reference dependencies by service name and avoid hardcoding IPs.
- Use
healthcheckplusdepends_on: condition: service_healthyfor reliable startup ordering. - Store secrets in an
.envfile or a secrets manager, not inline in the Compose file. - Use named volumes for stateful services so data survives
downwithout-v. - Split overrides into
docker-compose.override.ymlfor local development versus production.