Skip to main content

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 Hub
  • build — build from a local Dockerfile (. means the current directory)
  • ports"host:container" port mapping
  • environment — environment variables passed into the container
  • volumes — mount data into the container
  • depends_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.