What Is Docker Compose?
Docker Compose is a tool for defining and running multi-container applications. Instead of running multiple docker run commands with long flags you can never remember, you write a single docker-compose.yml file that describes your entire stack, then start everything with:
docker compose up
That's it. One command spins up your app, database, cache, and anything else — fully networked, with volumes mounted.
Why Docker Compose?
Running a typical web app requires at least:
- Your application server
- A database (PostgreSQL, MySQL, MongoDB)
- Maybe a cache (Redis)
- Maybe a reverse proxy (Nginx)
Without Compose, you'd run four separate docker run commands, manually create a network, and hope you remembered all the environment variables. With Compose, all of that is declared once in a file and repeatable.
Your First docker-compose.yml
version: "3.9"
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
postgres_data:
What this does:
app— builds from your localDockerfile, maps port 3000, waits fordbto startdb— runs PostgreSQL 16, sets credentials via env vars, persists data in a named volume- Both services share a private network automatically
Key Compose Concepts
Services
Each container is a service. Services are the main thing you configure in Compose.
Networks
By default, Compose creates a network for your project and all services join it. Services can reach each other by service name:
// In your app, connect to the database like this:
const db = new Client({ host: "db", port: 5432 });
// "db" resolves to the postgres container's IP
Volumes
Volumes persist data between container restarts. Without a volume, stopping the database container loses all your data.
volumes:
- postgres_data:/var/lib/postgresql/data # named volume (persisted)
- ./src:/app/src # bind mount (local files)
Environment Variables
Pass config via environment or reference an .env file:
services:
app:
env_file:
- .env
# .env
DATABASE_URL=postgresql://postgres:password@db:5432/myapp
SECRET_KEY=supersecret
Full Stack Example: Node + PostgreSQL + Redis
version: "3.9"
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "4000:4000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:secret@db:5432/appdb
- REDIS_URL=redis://cache:6379
volumes:
- .:/app
- /app/node_modules
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redisdata:/data
volumes:
pgdata:
redisdata:
Essential Docker Compose Commands
# Start all services (detached = in background)
docker compose up -d
# View running services
docker compose ps
# View logs
docker compose logs -f # all services
docker compose logs -f api # specific service
# Run a command inside a running container
docker compose exec api sh
docker compose exec db psql -U postgres
# Stop all services (keeps volumes)
docker compose down
# Stop and remove volumes (WARNING: deletes data)
docker compose down -v
# Rebuild images (after Dockerfile changes)
docker compose build
docker compose up -d --build
# Restart a single service
docker compose restart api
# Scale a service
docker compose up -d --scale api=3
Development vs Production Compose
Use multiple Compose files to override settings per environment:
docker-compose.yml (base):
services:
api:
image: myapp/api:latest
environment:
- NODE_ENV=production
docker-compose.override.yml (development — auto-loaded):
services:
api:
build: .
volumes:
- .:/app
environment:
- NODE_ENV=development
command: npm run dev
docker compose up automatically merges both files in development.
For production: docker compose -f docker-compose.yml up -d
Health Checks
Ensure services are actually ready (not just started) before dependent services connect:
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
api:
depends_on:
db:
condition: service_healthy # wait until pg_isready passes
Common Gotchas
Port already in use: Another process is using the port. Either stop it or change the host port mapping ("3001:3000").
Volume permission errors: On Linux, files created by containers may be owned by root. Fix with user: "${UID}:${GID}" in the service config.
Can't connect to database: The app starts before Postgres is ready. Use health checks with depends_on: condition: service_healthy.
Changes not reflected: Rebuilt image not used. Run docker compose up -d --build to force a rebuild.
Conclusion
Docker Compose transforms a complex multi-container setup into a single declarative file and a single command. For local development, it eliminates "works on my machine" problems by making your environment reproducible. For small to medium production deployments, it's a practical alternative to Kubernetes that's far simpler to operate.