StackPractices
beginner By Mathias Paulenko

Local Microservices Development with Docker Compose

Orchestrate multi-service local environments with Docker Compose including databases, caches, message brokers, and reverse proxies with hot reload and shared networks

Set up a complete local development environment for microservices using Docker Compose. Below is a practical approach to service definitions, shared networks, volume mounts for hot reload, environment configuration, and health checks that mirror production setups on developer machines.

When to Use This

  • Your application consists of multiple services that must run together locally. See Docker Basics for container fundamentals.
  • Developers need consistent environments regardless of host OS. See Environment Variables for per-environment configuration.
  • Databases, caches, and message brokers are required for integration testing. See Integration Testing for testing strategies.

Solution

1. Multi-Service Compose File

# docker-compose.yml
version: '3.8'

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./api:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://postgres:secret@db:5432/app
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - backend

  worker:
    build:
      context: ./worker
      dockerfile: Dockerfile.dev
    volumes:
      - ./worker:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgres://postgres:secret@db:5432/app
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    networks:
      - backend

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

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - backend

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.dev.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api
    networks:
      - backend

volumes:
  postgres_data:
  redis_data:

networks:
  backend:
    driver: bridge

2. Development Dockerfile with Hot Reload

# api/Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

EXPOSE 3000

CMD ["npm", "run", "dev"]

3. Override File for Local Customization

# docker-compose.override.yml
services:
  api:
    environment:
      - DEBUG=api:*
      - LOG_LEVEL=debug

  db:
    ports:
      - "5432:5432"

4. Health-Checked Startup Script

#!/bin/bash
# start-dev.sh

docker-compose up --build -d

echo "Waiting for services..."
until docker-compose exec -T db pg_isready -U postgres >/dev/null 2>&1; do
  sleep 1
done

echo "Running migrations..."
docker-compose exec api npx prisma migrate dev

echo "Seeding data..."
docker-compose exec api npm run seed

echo "Ready! API: http://localhost:3000"

How It Works

  • Services define each container with build context, image, or both
  • Networks enable DNS-based service discovery between containers
  • Volumes persist database data and enable host code mounts for hot reload
  • depends_on with condition: service_healthy waits for readiness, not just container start
  • override files merge with the base compose for local-specific settings

Variation: Compose Profiles for Selective Startup

services:
  monitoring:
    image: prom/prometheus
    profiles:
      - monitoring
    ports:
      - "9090:9090"
docker-compose --profile monitoring up

Production Considerations

  • Use .env files for secrets; never commit credentials to version control
  • Run docker-compose down -v to clean up volumes when switching branches
  • Keep images small with multi-stage builds for production Dockerfiles

Common Mistakes

  • Mounting node_modules from the host into the container, causing architecture mismatches
  • Forgetting condition: service_healthy, leading to connection errors on startup
  • Using latest tags for base images, causing non-reproducible builds

Performance Tips

  1. Use BuildKit for faster builds. Enable it in .env:
DOCKER_BUILDKIT=1
COMPOSE_DOCKER_CLI_BUILD=1
  1. Cache node_modules in a named volume. Avoid reinstalling on every rebuild:
services:
  api:
    volumes:
      - ./api:/app
      - api_node_modules:/app/node_modules

volumes:
  api_node_modules:
  1. Use tmpfs for temp files. Faster than disk-bound volumes:
services:
  api:
    tmpfs:
      - /tmp
      - /app/cache

Frequently Asked Questions

How is this different from Kubernetes?

Docker Compose is for single-host local development. Kubernetes orchestrates multi-node production clusters.

Can I use Docker Compose in CI/CD?

Yes, for integration tests. Use docker-compose -f docker-compose.yml -f docker-compose.ci.yml up to override settings for CI environments.