Docker Compose is a tool for defining and running multi-container applications on a single host. Instead of running a series of docker run commands with flags you have to remember, you describe the entire application — all its services, networks, and volumes — in a single YAML file. Compose reads that file and translates it into Docker API calls.
Compose = declarative multi-container orchestration on one host. You describe what should be running. Compose figures out how to get there.
The key word is declarative. Rather than writing steps (“create this network, then run this container”), you write the desired end state and let Compose handle the sequencing.
Core Concepts
| Concept | What it is |
|---|---|
| Project | Everything in one Compose file — the unit of isolation. Two projects can have services with the same name without conflict. |
| Service | A container definition — the image, config, ports, volumes. One service can run as multiple replicas. |
| Container | The running instance of a service. |
| Desired state | What the file declares should be running. docker compose up reconciles reality with this state. |
File Structure
A docker-compose.yml for a typical Django app with Postgres and Redis:
services:
web:
build: . # build from local Dockerfile
image: mydjango:latest
ports:
- "127.0.0.1:8000:8000" # loopback only — not exposed to outside the host
environment:
- DJANGO_SETTINGS_MODULE=myproject.settings
- DB_HOST=db
depends_on:
db:
condition: service_healthy # wait for pg_isready, not just container start
restart: unless-stopped
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myproject
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
retries: 5
volumes:
- db-data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
db-data:What each field maps to
| Compose field | Docker equivalent | Notes |
|---|---|---|
services | docker run | One entry per container type |
build | docker build | Runs before docker run if no image exists |
image | docker pull | Pulled if not present locally |
ports | -p HOST:CONTAINER | Exposes container ports on the host |
volumes | -v / --mount | Bind mounts or named volumes |
environment | -e KEY=VALUE | Env vars injected into the container |
networks | docker network create | Compose creates and wires these up |
depends_on | (manual ordering) | Controls startup order — see below |
restart | --restart policy | no, always, unless-stopped, on-failure |
Networking — the Most Useful Part of Compose
When you run docker compose up, Compose automatically:
- Creates a bridge network named
<project>_default - Attaches every service to it
- Registers each service name as a DNS hostname on that network
Your web container can reach db and cache by using those names as hostnames. No IP addresses, no manual network creation, no port mapping between containers.
# settings.py — use service names directly as hostnames
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": "db", # resolves to the db container
"NAME": "myproject",
"USER": "postgres",
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
}
}
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://cache:6379", # resolves to the cache container
}
}Networks are isolated per project — two different Compose projects won’t interfere even if their services have the same names.
depends_on — Startup Order vs Readiness
depends_on controls start order, not readiness. The database container starting and Postgres being ready to accept queries are two different things — Postgres takes a few seconds to initialise on first run. Django will crash on startup if it can’t connect.
# Wrong — container starts before Postgres is ready
web:
depends_on:
- db
# Right — wait for the healthcheck to pass
services:
db:
image: postgres:15
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
retries: 5
web:
depends_on:
db:
condition: service_healthyLifecycle Commands
# Start everything (build if needed, create networks/volumes, start containers)
docker compose up
# Start in background
docker compose up -d
# Stop and remove containers and networks (volumes preserved)
docker compose down
# Stop and remove everything including volumes (wipes the database)
docker compose down -v
# Rebuild images without layer cache
docker compose build --no-cache
# Show running containers in this project
docker compose ps
# Stream logs (all services or one)
docker compose logs -f
docker compose logs -f web
# Run management commands inside a running container
docker compose exec web python manage.py migrate
docker compose exec web python manage.py createsuperuser
docker compose exec web python manage.py collectstatic
# Pull latest images
docker compose pullWhat up does, in order
- Reads the Compose file and resolves desired state
- Creates any declared networks that don’t exist
- Creates any declared volumes that don’t exist
- Builds images where
build:is specified (if not already built) - Pulls images where
image:is specified (if not present locally) - Creates and starts containers in dependency order
down does the reverse: stops containers, removes them, removes networks. Volumes are left intact unless you pass -v — this is intentional so your database survives a down.
Port Binding — Loopback vs All Interfaces
ports:
- "8000:8000" # binds to 0.0.0.0 — accessible from anywhere the host is reachable
- "127.0.0.1:8000:8000" # binds to loopback — localhost only, not exposed to the networkIn production, bind your app port to 127.0.0.1 and put a reverse proxy (nginx, Caddy) in front. The proxy is the only thing that should be publicly reachable. Binding to 0.0.0.0 exposes the port to whatever networks the host is on — easy to do accidentally.
What Compose Does Not Do
Compose is a single-host, single-operator tool. It does not:
- Auto-scale — you can set
replicas: 3but Compose won’t add more workers when traffic increases - Self-heal across hosts — if the server dies, nothing restarts your containers elsewhere
- Schedule across a cluster — all containers run on one machine
- Route traffic away from unhealthy containers — it won’t stop sending work to a failing container
These are the problems Kubernetes solves. Compose and Kubernetes are not competing tools — they solve different problems at different scales.
| Situation | Tool |
|---|---|
| Single container | docker run |
| Multiple cooperating containers on one machine | Compose |
| Local Django dev with Postgres, Redis, Celery | Compose |
| Distributed system across multiple hosts | Kubernetes / Swarm |
| Production with auto-scaling and self-healing | Kubernetes |