Docker Compose lets you define a whole multi-container application — your API, its database, a cache, a queue — in a single declarative YAML file, then manage the entire stack with one command: docker compose up. It creates the network, resolves service names, wires volumes, and handles startup order for you.
Typical uses: local development environments, integration/system test environments, demos, and simple single-host deployments. It is not a cluster orchestrator — that is Kubernetes territory (see the Advanced page).
Command syntax note: Docker Compose v2 is a plugin invoked as docker compose (a space). The legacy v1 standalone binary was docker-compose (a hyphen). Use v2.
A complete example: a web API, a PostgreSQL database, and a Redis cache.
services:
api:
build: # build the image from a Dockerfile
context: ./api
dockerfile: Dockerfile
image: myapp/api:dev # tag given to the built image
ports:
- "8080:8000" # host:container
environment:
- DATABASE_URL=postgres://app:secret@db:5432/app
- REDIS_URL=redis://cache:6379
volumes:
- ./api:/code # bind mount: live code for development
depends_on:
db:
condition: service_healthy # wait until db is really ready
cache:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data # named volume: persists data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
cache:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- cache_data:/data
volumes:
db_data:
cache_data:
Top-level keys: services (the containers), volumes (named volumes), networks (custom networks), and name (project name). Each service is roughly a docker run translated to YAML.
image: postgres:16-alpine — pull (or reuse) a ready-made image from a registry.build: ./path — build from a Dockerfile; combine with image: to tag the result.docker compose build rebuilds; docker compose up --build rebuilds and starts in one go. Builds are cached just like plain docker build."8080:8000"; long form binds to a specific host IP: target: 8000, published: "8080". Quote the values — unquoted 5672:5672 can parse as YAML octal/base-60 and silently change meaning.db_data:/var/lib/postgresql/data) for persistent data; bind mounts (./api:/code) for live-reload development.env_file: .env for batches, and variable substitution from the shell/.env: - "DB_PASSWORD=${DB_PASSWORD}".depends_on controls start order, not readiness — Postgres being "started" does not mean it accepts connections yet. Combine healthchecks with conditions:
depends_on:
db:
condition: service_healthy # wait for the healthcheck to pass
cache:
condition: service_started # plain "it is running" is enough here
This is the Compose equivalent of waiting for dependencies — it removes most "app crashes at boot because the DB wasn't ready" flakiness.
db, cache — embedded DNS).services:
api:
networks: [frontend, backend]
db:
networks: [backend] # not reachable from "frontend" services
networks:
frontend:
backend:
internal: true # no outbound internet access
external: true networks connect Compose to pre-existing Docker networks (e.g., another project's network).docker compose up -d # create + start everything, detached
docker compose up -d api # start just one service (and its deps)
docker compose up --build # rebuild images first
docker compose ps # stack status
docker compose logs -f api # follow one service's logs
docker compose logs -f # follow everything, interleaved
docker compose exec api sh # shell inside a running service
docker compose run --rm api pytest # one-off command (new container)
docker compose stop # stop without removing
docker compose start # resume
docker compose restart api
docker compose down # stop + remove containers and networks
docker compose down -v # ... and volumes (DATA LOSS for db_data!)
docker compose config # validate + view the merged configuration
docker compose pull # pull latest images for all services
Know the difference: down removes containers (their writable layers) but keeps named volumes unless you pass -v. Most "I lost my database" accidents are down -v.
docker-compose.override.yml on top of docker-compose.yml. Common pattern: base file for the real stack, override for dev (bind mounts, debug ports). Explicit selection: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d.profiles: ["debug"] and start them only when wanted: docker compose --profile debug up.${DB_PASSWORD}); add it to .gitignore.myapp-api-1); set via top-level name:, -p, or COMPOSE_PROJECT_NAME. Different names = isolated copies of the stack on one machine.docker compose up -d --scale api=3 # 3 replicas behind the same network
docker compose run --rm api python manage.py migrate # one-off migration
docker compose run --rm api pytest -q # run tests in the stack
Scaling a service with a published host port conflicts (port already allocated) — for scaled services, remove fixed host ports and put a load balancer in front.
For a small, single-host deployment with modest availability needs — yes, especially with restart: unless-stopped and healthchecks. Compose limits become visible when you need:
At that point move to Kubernetes or a managed container service (ECS/EKS/AKS — see the Advanced page). Compose files also translate well: many teams use them as the source of truth for what runs, then deploy the same images to an orchestrator.
← Back to IT Knowledge Base