DevOps

Docker Compose for Local Development Environments

By Mohd Baquir Qureshi
Docker Shipping Containers

"It works on my machine!" is the oldest excuse in software engineering. When a new developer joins your team, they shouldn't spend three days configuring PostgreSQL, Redis, Node.js, and Python environments. With Docker Compose, onboarding should take exactly one command: docker-compose up.

The Anatomy of a Development docker-compose.yml

A good development compose file differs from a production one. In production, code is baked into the image. In development, you want to use Volumes to mount your local source code into the container, enabling hot-reloading.

version: '3.8'

services:
  api:
    build: 
      context: ./backend
      target: development # Multi-stage build targeting dev
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app # Hot-reloading magic
      - /app/node_modules # Anonymous volume to prevent local node_modules from overwriting container's
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app_db
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app_db
    ports:
      - "5432:5432" # Expose so you can connect via DataGrip/DBeaver
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Key Best Practices

1. Anonymous Volumes for Dependencies

Notice the - /app/node_modules in the api service. When you bind-mount your local directory to /app, your local (macOS/Windows) node_modules folder overrides the container's (Linux) folder. This causes native C++ bindings (like bcrypt) to crash. The anonymous volume tells Docker to use the container's node_modules instead.

2. Using depends_on with Healthchecks

depends_on only waits for the container to start, not for the process inside it to be ready. If your API tries to connect to PostgreSQL while it's still initializing, it will crash. Use healthchecks for robust startup ordering.

  db:
    image: postgres:15-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app_db"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    depends_on:
      db:
        condition: service_healthy

Overriding for Production

Never put production secrets or development volumes in your base docker-compose.yml. Use a base file for common services, and create docker-compose.override.yml (which compose reads automatically) for local development, and docker-compose.prod.yml for production deployments.

Conclusion

Docker Compose eliminates the friction of local environment setup. By encapsulating your app, database, cache, and message queues into a single declarative file, you guarantee parity across all developer machines and significantly reduce the "it works on my machine" syndrome.