Docker Compose shines for local development. Spin up your entire stack — application, database, cache, message queue — with one command. But there’s a difference between a Compose setup that works and one that’s pleasant to use.

Project Structure

project/
├── docker-compose.yml        # Core services
├── docker-compose.dev.yml    # Dev overrides (hot reload, debug ports)
├── .env                      # Environment variables
├── Dockerfile                # Production build
└── Dockerfile.dev            # Dev build (with hot reload)

Separate your dev configuration from production. Use override files:

docker compose -f docker-compose.yml -f docker-compose.dev.yml up

Hot Reload with Volume Mounts

The key to a good dev experience is live code reloading:

# docker-compose.dev.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/app/src
      - /app/node_modules  # Don't mount node_modules from host
    environment:
      - NODE_ENV=development

Mount your source code but exclude node_modules. This avoids platform mismatches (native modules built for macOS won’t work in a Linux container).

Health Checks and Dependencies

Don’t rely on depends_on alone. It only waits for the container to start, not for the service to be ready:

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    depends_on:
      db:
        condition: service_healthy

Now the API container waits until PostgreSQL is actually accepting connections.

Named Volumes for Data Persistence

volumes:
  postgres_data:
  redis_data:

services:
  db:
    image: postgres:16
    volumes:
      - postgres_data:/var/lib/postgresql/data

Named volumes persist across docker compose down. Your database data survives restarts. Use docker compose down -v when you explicitly want a clean slate.

Development Seed Data

Add an init script for your database:

services:
  db:
    image: postgres:16
    volumes:
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql

Every team member gets the same starting data. No more “it works on my machine” because someone forgot to run migrations.

Useful Compose Commands

# Start in background
docker compose up -d

# View logs for one service
docker compose logs -f api

# Run a one-off command
docker compose exec api npm run migrate

# Rebuild after dependency changes
docker compose up --build api

# Clean everything
docker compose down -v --remove-orphans

A well-configured Docker Compose setup eliminates the “20 steps to set up the dev environment” README. New team members run one command and they’re productive.