BytePane

Docker Compose for Local Development: Complete Guide

Docker15 min read

Why Docker Compose for Local Development?

Modern applications rarely run in isolation. A typical web app might need a Node.js server, a PostgreSQL database, a Redis cache, and perhaps an Elasticsearch instance. Docker Compose lets you define all of these services in a single YAML file and start them with one command: docker compose up.

The key benefits for local development are reproducibility (every developer gets the exact same environment), isolation (no conflicting versions of PostgreSQL or Node.js installed globally), and simplicity (new team members run one command instead of following a 20-step setup guide).

As of 2026, Docker Compose V2 is the standard. It is integrated into the Docker CLI as docker compose (no hyphen). The older docker-compose (with hyphen) is deprecated. All examples in this guide use the V2 syntax.

Your First docker-compose.yml

A Compose file defines services, networks, and volumes. Here is a minimal example for a Node.js app with PostgreSQL and Redis.

# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules    # Prevent overwriting container's node_modules
    environment:
      - DATABASE_URL=postgres://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

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

volumes:
  pgdata:

You can validate the YAML syntax of your Compose file by converting it to JSON with our JSON Formatter (paste the YAML output of docker compose config). For a deeper comparison of YAML and JSON formats, see our YAML vs JSON guide.

Development-Optimized Dockerfile

Your development Dockerfile should prioritize fast rebuilds and hot reloading, not production optimization. Use a separate Dockerfile.dev for development.

# Dockerfile.dev
FROM node:22-alpine

WORKDIR /app

# Install dependencies first (cached layer)
COPY package.json package-lock.json ./
RUN npm ci

# Copy source code (changes frequently, so this layer is rebuilt often)
COPY . .

# Use nodemon or tsx for hot reloading
CMD ["npx", "tsx", "watch", "src/index.ts"]

The key optimization is separating package.json installation from source code copying. Docker caches layers, so if your dependencies have not changed, npm ci will be skipped on subsequent builds. For production Dockerfiles with multi-stage builds, see our Docker Container Best Practices guide.

Volumes and Hot Reloading

The most important feature for local development is hot reloading. You want your code changes to be reflected inside the container instantly, without rebuilding the image. Bind mounts make this possible.

services:
  app:
    volumes:
      # Bind mount: host directory → container directory
      - .:/app

      # Anonymous volume: preserve container's node_modules
      # (prevents host node_modules from overwriting container's)
      - /app/node_modules

      # Named volume for build cache
      - build_cache:/app/.next

  # For frontend with Vite:
  frontend:
    volumes:
      - ./frontend:/app
      - /app/node_modules
    environment:
      # Vite needs this for HMR to work through Docker
      - CHOKIDAR_USEPOLLING=true
      - WATCHPACK_POLLING=true

volumes:
  build_cache:

On macOS and Windows, bind mount performance can be slow because Docker Desktop uses a virtual machine. Docker Desktop 4.x+ includes VirtioFS, which dramatically improves file system performance. Ensure it is enabled in Docker Desktop settings under "General" and "Use VirtioFS."

Docker Compose Watch (2026 Recommended)

Docker Compose 2.22+ introduced docker compose watch, which is now the recommended approach for development. It syncs files, rebuilds on dependency changes, and restarts services automatically.

services:
  app:
    build: .
    develop:
      watch:
        # Sync source code changes without rebuild
        - action: sync
          path: ./src
          target: /app/src

        # Rebuild when dependencies change
        - action: rebuild
          path: package.json

        # Restart on config changes
        - action: sync+restart
          path: ./config
          target: /app/config

# Run with:
# docker compose watch

Networking Between Services

Docker Compose creates a default network for all services in the Compose file. Services can reach each other by their service name as the hostname. For example, the app service can connect to PostgreSQL at db:5432 and Redis at cache:6379.

services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    networks:
      - frontend-net

  api:
    build: ./api
    ports:
      - "4000:4000"
    networks:
      - frontend-net
      - backend-net

  db:
    image: postgres:16-alpine
    networks:
      - backend-net
    # No ports exposed to host — only accessible from backend-net

networks:
  frontend-net:
  backend-net:

# frontend can reach api, but NOT db
# api can reach both frontend and db
# db can only be reached from backend-net

Custom networks let you isolate services. In this example, the frontend cannot directly access the database, which mirrors a production architecture. For DNS-related configuration, check our DNS Records Explained guide.

Environment Variables and Secrets

Docker Compose supports multiple ways to pass environment variables to your services. For local development, .env files are the most convenient approach.

# .env file (automatically loaded by Docker Compose)
POSTGRES_DB=myapp
POSTGRES_USER=postgres
POSTGRES_PASSWORD=secret
NODE_ENV=development
API_PORT=4000

# docker-compose.yml
services:
  api:
    build: .
    env_file:
      - .env              # Default env vars
      - .env.local         # Local overrides (gitignored)
    environment:
      # Inline vars override env_file
      - DEBUG=true

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Always add .env.local to your .gitignore. Commit a .env.example with placeholder values so new developers know which variables to set. For a deeper dive, read our Environment Variables Guide.

Database Seeding and Migrations

Starting with an empty database is rarely useful for development. Docker Compose makes it easy to seed initial data and run migrations automatically.

services:
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
      # Files in this directory run on first container creation
      - ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql
      - ./db/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql

  # Run migrations as a one-off service
  migrate:
    build: .
    command: npx prisma migrate deploy
    depends_on:
      db:
        condition: service_healthy
    profiles:
      - setup    # Only runs when explicitly requested

# Run setup:
# docker compose --profile setup up migrate

# Reset database (destroy and recreate):
# docker compose down -v   # -v removes named volumes
# docker compose up

The profiles feature is useful for services that should not run by default. Migration services, test runners, and one-off scripts can be placed in profiles and invoked only when needed.

Debugging Inside Containers

Debugging containerized applications requires exposing debug ports and configuring your IDE to attach to the container's debugger.

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
      - "9229:9229"   # Node.js debug port
    command: node --inspect=0.0.0.0:9229 src/index.js
    # Or for tsx:
    # command: npx tsx --inspect=0.0.0.0:9229 watch src/index.ts

# VS Code launch.json for attaching to container:
# {
#   "type": "node",
#   "request": "attach",
#   "name": "Docker: Attach",
#   "port": 9229,
#   "address": "localhost",
#   "localRoot": "${workspaceFolder}",
#   "remoteRoot": "/app",
#   "restart": true
# }

# Shell into a running container for manual debugging:
# docker compose exec app sh
# docker compose exec db psql -U postgres -d myapp

The docker compose exec command is invaluable for troubleshooting. You can open a shell, run database queries, check logs, or test network connectivity from inside any container.

Essential Docker Compose Commands

CommandPurpose
docker compose up -dStart all services in detached mode
docker compose downStop and remove containers (keeps volumes)
docker compose down -vStop, remove containers AND volumes (full reset)
docker compose logs -f appFollow logs for a specific service
docker compose exec app shOpen a shell inside a running container
docker compose build --no-cacheRebuild images without cache
docker compose psList running services and their ports
docker compose watchStart with file sync and auto-rebuild

Common Service Recipes

Here are ready-to-use Compose configurations for popular development dependencies.

MongoDB

  mongo:
    image: mongo:7
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: secret
    volumes:
      - mongodata:/data/db

Elasticsearch

  elasticsearch:
    image: elasticsearch:8.15.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - esdata:/usr/share/elasticsearch/data

Mailpit (Email Testing)

  mailpit:
    image: axllent/mailpit
    ports:
      - "8025:8025"   # Web UI
      - "1025:1025"   # SMTP
    # Point your app's SMTP to mailpit:1025
    # View caught emails at http://localhost:8025

Best Practices Summary

  1. Use separate Dockerfiles -- Dockerfile.dev for development, Dockerfile for production.
  2. Use healthchecks on dependencies -- Ensure your app does not start before the database is ready.
  3. Pin image versions -- Use postgres:16-alpine not postgres:latest for reproducibility.
  4. Use named volumes for data persistence -- Anonymous volumes are lost when containers are removed.
  5. Use .dockerignore -- Exclude node_modules, .git, and build artifacts from the Docker build context.
  6. Commit docker-compose.yml -- This file is documentation for your development environment and should be version-controlled.
  7. Use Docker Compose Watch -- Prefer docker compose watch over bind mounts for better cross-platform performance.
  8. Document the setup -- Include a "Getting Started" section in your project README with the exact commands to run.

Validate Your Configuration Files

Docker Compose YAML files are prone to indentation errors. Use our JSON Formatter to validate configuration, or check your environment variable syntax with our developer tools.

Open JSON Formatter

Related Articles