Docker Compose lets you define and run multi-container applications with a single YAML file and a single command. This guide walks you from an empty directory to a fully running web app with a database, cache, and background worker — all orchestrated by Compose.
Running a single Docker container is straightforward. But real applications have multiple services: a web server, a database, a caching layer, a message queue. Coordinating these manually with individual docker run commands — remembering all the ports, volumes, networks, and environment variables — is tedious and error-prone.
Docker Compose solves this by letting you describe your entire multi-container application in a single docker-compose.yml file. From there, one command starts everything, another stops it, and the configuration is version-controlled alongside your code.
Docker Compose V2 is included with Docker Desktop (Mac/Windows). On Linux:
# Check if already installed
docker compose version
# Install on Ubuntu/Debian if missing
sudo apt update && sudo apt install docker-compose-plugin
Note: The modern command is docker compose (no hyphen), which is the V2 plugin. The older standalone docker-compose (with hyphen) is V1 and deprecated.
Let's build a simple Node.js app with a PostgreSQL database. Create a docker-compose.yml in your project root:
version: '3.9'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://postgres:password@db:5432/myapp
depends_on:
db:
condition: service_healthy
volumes:
- .:/app
- /app/node_modules
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
Each key under services is a container. The name (app, db) becomes the hostname on the internal Docker network — this is why the database URL uses @db:5432 rather than @localhost:5432.
build: . tells Compose to build an image from the Dockerfile in the current directory. image: postgres:15-alpine pulls a pre-built image from Docker Hub. Use build for your own services and image for third-party services.
"3000:3000" maps host port 3000 to container port 3000. Format is HOST:CONTAINER. Only map ports you need to expose to your host machine — containers communicate with each other over the internal network without port mapping.
Named volumes like postgres_data persist data across container restarts. Bind mounts like .:/app mount your local directory into the container for live code reloading during development.
depends_on with condition: service_healthy ensures your app does not start until the database is ready to accept connections — not just started. Without this, your app may crash on startup because it cannot connect to a database that is still initializing.
# Start all services in the background
docker compose up -d
# View running containers and their status
docker compose ps
# Stream logs from all services
docker compose logs -f
# Stream logs from one service
docker compose logs -f app
# Execute a command inside a running container
docker compose exec db psql -U postgres myapp
# Stop and remove containers (keeps volumes)
docker compose down
# Stop and remove everything including volumes
docker compose down -v
# Rebuild images and restart
docker compose up -d --build
Never hard-code secrets in your docker-compose.yml. Instead, use a .env file (which you .gitignore) and reference variables with ${VARIABLE} syntax:
# .env file (gitignored)
POSTGRES_PASSWORD=supersecret
JWT_SECRET=my-jwt-signing-key
# docker-compose.yml
services:
db:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Compose automatically loads the .env file from the same directory as docker-compose.yml.
Extending the previous example with a Redis cache:
services:
app:
# ... (same as before)
environment:
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
cache:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
Docker Compose is excellent for development and single-server deployments. For production on a single server, it works well. For multi-server deployments with high availability requirements, consider Docker Swarm or Kubernetes. Key production practices:
postgres:15.4-alpine not postgres:latest)mem_limit: 512m and cpus: '0.5'restart: unless-stopped