What is Docker Compose?
The Problem with Multiple docker run Commands
A Node.js app with a PostgreSQL database requires two containers. Without Compose, you'd run something like:
docker network create myapp-net
docker run -d \
--name postgres \
--network myapp-net \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16
docker run -d \
--name api \
--network myapp-net \
-p 3000:3000 \
-e DATABASE_URL=postgres://postgres:secret@postgres:5432/mydb \
myapp:latest
Now multiply this across dev, staging, and production. And add Redis. And Nginx. Docker Compose replaces all of that with a single YAML file.
The docker-compose.yml File
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/mydb
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
This file defines two services (api and db). Compose handles networking automatically — services can reach each other by their service name (so api connects to postgres://db:5432).
Core Concepts
Services
Each entry under services is a container. You configure it with:
image— use a pre-built image from Docker Hubbuild— build from a localDockerfile(.means the current directory)ports—"host:container"port mappingenvironment— environment variables passed into the containervolumes— mount data into the containerdepends_on— start order (note: doesn't wait for the service to be ready)
Volumes
Named volumes (like pgdata) persist data between container restarts. Without it, your database data disappears when the container is removed.
volumes:
pgdata: # declare the volume at the top level
db:
volumes:
- pgdata:/var/lib/postgresql/data # mount it in the service
Networks
Compose creates a default bridge network for your project. All services join it automatically, so they can talk to each other by service name — no manual docker network create needed.
Environment Files
Keep secrets out of docker-compose.yml with a .env file:
# .env
POSTGRES_PASSWORD=mysecretpassword
DATABASE_URL=postgres://postgres:mysecretpassword@db:5432/mydb
# docker-compose.yml
services:
api:
env_file:
- .env
Add .env to .gitignore — never commit it.
Essential Commands
docker compose up # start all services (foreground)
docker compose up -d # start in background (detached)
docker compose down # stop and remove containers
docker compose down -v # also remove volumes (deletes DB data!)
docker compose logs -f api # stream logs for the api service
docker compose exec api sh # open a shell inside the api container
docker compose ps # list running services and their status
docker compose build # rebuild images (after Dockerfile changes)
docker compose restart api # restart a single service
A Real-World Example
services:
api:
build: .
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/mydb
REDIS_URL: redis://cache:6379
depends_on:
- db
- cache
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
cache:
image: redis:7-alpine
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- api
restart: unless-stopped
volumes:
pgdata:
restart: unless-stopped ensures containers come back up after a server reboot or crash — the Compose equivalent of PM2's startup script.