If you've ever spent an afternoon helping a teammate set up PostgreSQL, Redis, and three microservices just to run the project locally, Docker Compose is the solution. It lets you define your entire development stack in a single YAML file and start everything with docker compose up.
This guide covers the Docker Compose patterns I use daily for full-stack web development, with practical examples for common setups.
A Complete Development Stack
Here's a realistic compose.yml for a Next.js application with a PostgreSQL database, Redis cache, and a Python API service:
compose.yml
services:
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
api:
build:
context: ./api
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
volumes:
- ./api:/app
environment:
DATABASE_URL: postgresql://app:devpassword@db:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
web:
build:
context: ./web
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- ./web:/app
- /app/node_modules
environment:
API_URL: http://api:8000
volumes:
postgres_data:
Key Patterns
Health Checks and Dependency Ordering
The depends_on with condition: service_healthy pattern is essential. Without health checks, Docker only waits for the container to start, not for the service inside it to be ready. A PostgreSQL container that started 100ms ago will reject connections, causing your API to crash on boot.
Volume Mounts for Hot Reload
Mounting your source code as a volume (./api:/app) means changes you make locally are immediately reflected inside the container. Combined with a development server that watches for file changes (uvicorn with --reload, Next.js dev server), you get the same hot-reload experience as running locally.
The /app/node_modules anonymous volume prevents your local node_modules from overwriting the container's — this is important when your host OS differs from the container OS (e.g., macOS host, Linux container).
Environment Variables
Notice that the API service uses db as the hostname to reach PostgreSQL, not localhost. Docker Compose creates a network where services can reach each other by their service name. This is the most common source of confusion for developers new to Docker.
Development vs. Production Dockerfiles
Use separate Dockerfiles for development and production:
Dockerfile.dev (development)
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
Dockerfile (production)
FROM node:24-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-slim AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
CMD ["node", "server.js"]
The development Dockerfile installs all dependencies (including devDependencies) and runs the dev server. The production Dockerfile uses a multi-stage build to create a minimal image with only what's needed to run the application.
Useful Commands
Common Docker Compose commands
# Start all services in the background
docker compose up -d
# View logs from a specific service
docker compose logs -f api
# Run a one-off command in a service
docker compose exec db psql -U app -d myapp
# Rebuild after changing a Dockerfile
docker compose up -d --build api
# Stop and remove everything (including volumes)
docker compose down -v
# Scale a service (e.g., multiple API workers)
docker compose up -d --scale api=3
Pro Tip: Add a
Makefilewith common commands so teammates don't need to remember Docker Compose syntax.make devis easier thandocker compose up -d --build.
When Not to Use Docker Compose
Docker Compose is great for development environments but isn't the right tool for everything:
- If you're deploying to Vercel, AWS Lambda, or similar serverless platforms, your production deployment doesn't use Docker at all — use Compose only for local development.
- For orchestrating containers in production across multiple machines, use Kubernetes, ECS, or a similar production orchestrator.
- If your project is a single frontend app with no backend dependencies, Docker Compose adds complexity without much benefit — just run
npm run dev.
