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

ConceptWhat it is
ProjectEverything in one Compose file — the unit of isolation. Two projects can have services with the same name without conflict.
ServiceA container definition — the image, config, ports, volumes. One service can run as multiple replicas.
ContainerThe running instance of a service.
Desired stateWhat 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 fieldDocker equivalentNotes
servicesdocker runOne entry per container type
builddocker buildRuns before docker run if no image exists
imagedocker pullPulled if not present locally
ports-p HOST:CONTAINERExposes container ports on the host
volumes-v / --mountBind mounts or named volumes
environment-e KEY=VALUEEnv vars injected into the container
networksdocker network createCompose creates and wires these up
depends_on(manual ordering)Controls startup order — see below
restart--restart policyno, always, unless-stopped, on-failure

Networking — the Most Useful Part of Compose

When you run docker compose up, Compose automatically:

  1. Creates a bridge network named <project>_default
  2. Attaches every service to it
  3. 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_healthy

Lifecycle 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 pull

What up does, in order

  1. Reads the Compose file and resolves desired state
  2. Creates any declared networks that don’t exist
  3. Creates any declared volumes that don’t exist
  4. Builds images where build: is specified (if not already built)
  5. Pulls images where image: is specified (if not present locally)
  6. 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 network

In 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: 3 but 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.

SituationTool
Single containerdocker run
Multiple cooperating containers on one machineCompose
Local Django dev with Postgres, Redis, CeleryCompose
Distributed system across multiple hostsKubernetes / Swarm
Production with auto-scaling and self-healingKubernetes

See Also