edit_document// BLOG_POST.md

Docker Compose for Local Development: Replace Your Setup Scripts Forever

//

, ,

Every developer has experienced it: you clone a repo, scan a two-page setup guide, install three database engines, configure environment variables, and spend half a day before writing a single line of code. Docker Compose eliminates that entire category of friction. One YAML file defines your full stack. One command starts everything. New teammates go from clone to running in under five minutes.

Why Docker Compose for Local Dev

Docker Compose is not just for production deployments. Its strongest use case is local development environments. Instead of installing PostgreSQL, Redis, Elasticsearch, and RabbitMQ directly on your machine (each with version conflicts, port collisions, and platform quirks), you declare them as services in a compose.yaml file. Each service runs in isolation with a pinned version. Tear it down, rebuild it, or switch branches with different infrastructure requirements without touching your host OS.

A Real-World Compose File

Here is a compose file for a typical web application with a Node.js API, PostgreSQL database, Redis cache, and a background worker:

# compose.yaml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules  # Prevent host node_modules from overriding container
    environment:
      DATABASE_URL: postgres://appuser:devpass@db:5432/appdb
      REDIS_URL: redis://cache:6379
      NODE_ENV: development
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    command: npm run dev

  worker:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      DATABASE_URL: postgres://appuser:devpass@db:5432/appdb
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
    command: npm run worker

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

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru

volumes:
  pgdata:

Key Patterns Worth Knowing

Health checks with depends_on conditions. The depends_on directive with condition: service_healthy ensures your API does not start until PostgreSQL is actually accepting connections, not just when the container starts. Without this, you get intermittent connection-refused errors on startup.

Volume mounts for live reload. Mounting your source code (.:/app) means file changes on your host immediately reflect inside the container. Combined with a file-watching dev server (nodemon, Vite, webpack-dev-server), you get instant feedback without rebuilding the image.

Named volumes for persistence. The pgdata volume persists your database between docker compose down and docker compose up cycles. Your data survives restarts. Use docker compose down -v when you want a clean slate.

The Development Dockerfile

# Dockerfile.dev -- optimized for local development, not production
FROM node:22-alpine
WORKDIR /app

# Install dependencies first for better layer caching
COPY package*.json ./
RUN npm install

# Source code is mounted via volume, so no COPY . . needed
EXPOSE 3000
CMD ["npm", "run", "dev"]

Notice there is no COPY . . for the source code. Since we mount the working directory as a volume, the container reads your local files directly. The Dockerfile only installs dependencies into the image layer for caching.

Useful Commands

# Start everything in the background
docker compose up -d

# Watch logs from all services
docker compose logs -f

# Watch logs from a specific service
docker compose logs -f api

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

# Rebuild images after Dockerfile changes
docker compose up -d --build

# Stop everything, preserve volumes
docker compose down

# Nuclear option: stop everything, delete volumes, images
docker compose down -v --rmi local

Environment Variable Management

For sensitive values or values that differ per developer, use an .env file alongside your compose file. Docker Compose automatically reads it:

# .env (git-ignored)
POSTGRES_PASSWORD=devpass
API_PORT=3000
REDIS_MAX_MEMORY=128mb

Reference these in compose.yaml with ${VARIABLE_NAME} syntax. Ship a .env.example with placeholder values so new developers know which variables to set.

When Compose Is Not Enough

Docker Compose is ideal for local development and small-scale deployments. For production orchestration at scale, Kubernetes is the standard. For development environments that need to bridge into cloud resources, tools like Telepresence or Tilt extend the Compose model. But for the core use case of “clone, compose up, start coding,” nothing beats a well-structured compose file.

Further reading: Docker Compose Docs | Compose File Reference | Docker Dev Best Practices


arrow_circle_right// POST_NAVIGATION

forum// COMMENTS

Leave a Reply

Your email address will not be published. Required fields are marked *