Skip to main content

Docker Compose

A tool for defining and running multi-container Docker applications using a YAML file. A single docker compose up command creates and starts all services, networks, and volumes defined in the compose file.

Docker Compose — One Command to Run Your Entire Application Stack

What Is Docker Compose in Simple Terms?

Docker Compose solves the problem of running multi-container applications. A real application is never just one container — it needs a web server, a database, a cache, and maybe a message queue. Without Compose, you would start each container separately with individual docker run commands, manage their networks manually, and remember all the flags and configuration every time.

With Docker Compose, you write one YAML file that describes your entire application stack — what containers to run, how they connect, where data is stored — and a single docker compose up command starts everything at once. One command to start, one command to stop, one file to understand the whole application.

Bash
+------------------------------------------+
| docker-compose.yml |
| |
| services: |
| api: <- payment API container |
| postgres: <- database container |
| redis: <- cache container |
| |
| networks: |
| app-network: <- how they connect |
| |
| volumes: |
| postgres-data: <- where data persists |
+------------------------------------------+
|
| docker compose up -d
v
+------------------------------------------+
| Three containers running |
| All on app-network (DNS works) |
| Data persisted in postgres-data volume |
| api reaches postgres by name: postgres |
+------------------------------------------+

A Complete docker-compose.yml Example

YAML
version: "3.8"
services:
payment-api:
image: registry.razorpay.in/payment-api:v3.1.0
ports:
- "8080:8080" # host:container
environment:
NODE_ENV: production
DB_HOST: postgres # use service name as hostname
REDIS_HOST: redis
depends_on:
postgres:
condition: service_healthy # wait for postgres to be ready
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.5"
memory: 512M
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: payments
POSTGRES_USER: api_user
POSTGRES_PASSWORD: secret
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U api_user -d payments"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
postgres-data: # Docker creates and manages this
redis-data:

Essential Compose Commands

Bash
# Start all services in the background
docker compose up -d
# Start and rebuild images first
docker compose up -d --build
# View status of all services
docker compose ps
# NAME STATUS PORTS
# payment-api Up 5 minutes 0.0.0.0:8080->8080/tcp
# postgres Up 5 minutes (healthy)
# redis Up 5 minutes
# View logs from all services
docker compose logs
# Follow logs from a specific service
docker compose logs -f payment-api
# Run a command in a running service
docker compose exec postgres psql -U api_user
# Stop and remove containers (keeps volumes = keeps data)
docker compose down
# Stop and remove containers AND volumes (DELETES ALL DATA)
docker compose down -v
# WARNING: This permanently deletes all volume data
# Restart a specific service
docker compose restart payment-api
# Scale a service to multiple instances
docker compose up -d --scale payment-api=3
# Pull latest images for all services
docker compose pull
# Build images that have a build context
docker compose build
# See resource usage
docker compose top

Service Discovery in Compose

Docker Compose automatically creates a user-defined bridge network for the project. Every service can reach every other service using the service name as the hostname:

YAML
services:
api:
environment:
DB_HOST: postgres # reaches the postgres service
REDIS_HOST: redis # reaches the redis service
AMQP_HOST: rabbitmq # reaches rabbitmq
Bash
# Verify from inside the api container
docker compose exec api ping postgres
# PING postgres (172.20.0.3)
# Service names are DNS hostnames automatically

Compose v1 vs Compose v2

Bash
# Compose v1 (old — separate binary)
docker-compose up # uses docker-compose command
# Compose v2 (current — built into Docker CLI)
docker compose up # uses docker compose command (space not hyphen)
# Check your version
docker compose version
# Docker Compose version v2.23.0
# Always use v2 (docker compose) on modern systems
# The compose file format is identical between versions

Environment Variables and Secrets

Bash
# .env file — loaded automatically by Compose
# Add to .gitignore — never commit secrets
DB_PASSWORD=supersecret
API_KEY=abc123
NODE_ENV=production
# Reference in docker-compose.yml
services:
api:
environment:
DB_PASSWORD: ${DB_PASSWORD} # from .env file
API_KEY: ${API_KEY}
# Verify variables are loaded correctly
docker compose config
# Shows fully resolved compose file with all substitutions applied

Common Mistakes

Mistake Consequence Fix
docker compose down -v carelessly All database data deleted permanently Know what -v does — only use intentionally
No depends_on with health checks API crashes because DB not ready Add condition: service_healthy with healthcheck
Hardcoding secrets in compose file Secrets committed to git Use .env file (in .gitignore) with ${VAR}
No resource limits One service starves others Always set deploy.resources.limits
Using container_name with scaling Name conflict when scaling Remove container_name for scalable services
Tip

Run docker compose config to see the fully resolved compose file with all variable substitutions applied. This is the fastest way to verify your .env file is being read correctly and that all service configurations look exactly as expected before starting the stack.

Remember

docker compose down keeps your volumes and data. docker compose down -v deletes your volumes and all data inside them permanently. This is the most common cause of accidental data loss with Docker Compose. Know exactly which flag you are using.

Common Mistake

Using depends_on without the condition: service_healthy clause. Basic depends_on: postgres only waits for the container to start — not for PostgreSQL to be ready to accept connections. PostgreSQL takes 2-5 seconds to initialise after container start. Without the health check condition, your API will try to connect before the database is ready and crash.

Security

Never put secrets directly in docker-compose.yml — they end up in version control. Use a .env file for local development and a proper secret manager (AWS Secrets Manager, HashiCorp Vault) for production. The .env file must be in your .gitignore from day one.

Frequently Asked Questions

What problem does Compose solve that a shell script with docker run commands doesn't?

Wiring up a multi-container app by hand means remembering network names, volume mounts, environment variables, and startup order across several `docker run` invocations, and re-typing them correctly every time. Compose captures all of that declaratively in one YAML file, and `docker compose up` recreates the entire stack — networks, volumes, and inter-service DNS — from a clean state reproducibly, which is why it became the default way to run local dev environments with a database, cache, and app together.

Where does Compose stop being the right tool?

Compose runs everything on a single Docker host with no built-in self-healing across machines, no rolling updates, and no auto-scaling — if that host dies, everything on it goes down together. It's excellent for local development, CI test environments, and small single-server deployments, but production workloads that need multi-host resilience should move to Kubernetes or Docker Swarm. A common mistake is treating a Compose-based 'production' setup as durable when it has no failover story at all.