Docker Compose for Local Development of Microservices

devops docker microservices

Running one service locally is easy. Running ten—with their databases, message queues, and dependencies—is a nightmare. Docker Compose solves this.

The Problem

Your microservices architecture:

┌─────────┐     ┌─────────┐     ┌─────────┐
│ API     │ ──▶ │ Users   │ ──▶ │ Postgres│
│ Gateway │     │ Service │     └─────────┘
└─────────┘     └─────────┘


┌─────────┐     ┌─────────┐
│ Orders  │ ──▶ │ Redis   │
│ Service │     └─────────┘
└─────────┘


┌─────────┐
│ RabbitMQ│
└─────────┘

Running this locally means: 4 services, 3 databases/queues, environment variables, network configuration…

Docker Compose to the Rescue

# docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "8000:8000"
    environment:
      - USERS_SERVICE_URL=http://users-service:8001
      - ORDERS_SERVICE_URL=http://orders-service:8002
    depends_on:
      - users-service
      - orders-service

  users-service:
    build: ./users-service
    ports:
      - "8001:8001"
    environment:
      - DATABASE_URL=postgres://user:pass@postgres:5432/users
    depends_on:
      - postgres

  orders-service:
    build: ./orders-service
    ports:
      - "8002:8002"
    environment:
      - REDIS_URL=redis://redis:6379
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - redis
      - rabbitmq

  postgres:
    image: postgres:13
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=users
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:6
    ports:
      - "6379:6379"

  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"

volumes:
  postgres_data:

One command: docker-compose up

Development Workflow

Starting Everything

# Start all services
docker-compose up

# Start in background
docker-compose up -d

# Start specific services
docker-compose up api-gateway users-service

Hot Reload with Volumes

services:
  users-service:
    build: ./users-service
    volumes:
      - ./users-service:/app  # Mount source code
      - /app/node_modules     # Exclude node_modules
    command: npm run dev      # Run dev server with watch

Edit code locally, changes reflected immediately.

Viewing Logs

# All logs
docker-compose logs -f

# Specific service
docker-compose logs -f users-service

# Last 100 lines
docker-compose logs --tail=100 users-service

Running Commands

# Run in new container
docker-compose run users-service npm test

# Run in existing container
docker-compose exec users-service bash

# Database migrations
docker-compose exec users-service python manage.py migrate

Development vs Production

Override Files

# docker-compose.yml (base)
services:
  api:
    build: ./api
    
# docker-compose.override.yml (dev, auto-loaded)
services:
  api:
    volumes:
      - ./api:/app
    environment:
      - DEBUG=true
    command: npm run dev

# docker-compose.prod.yml (production)
services:
  api:
    environment:
      - DEBUG=false
    command: npm start
# Dev (uses override automatically)
docker-compose up

# Prod
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

Service Discovery

Docker Compose creates a network where services can reach each other by name:

# In orders-service
import redis
r = redis.Redis(host='redis', port=6379)  # 'redis' is the service name

Health Checks

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

  users-service:
    build: ./users-service
    depends_on:
      postgres:
        condition: service_healthy

Service starts only when dependency is healthy.

Environment Variables

.env File

# .env
POSTGRES_PASSWORD=supersecret
API_KEY=abc123
# docker-compose.yml
services:
  api:
    environment:
      - API_KEY=${API_KEY}

Multiple Environments

# .env.development
DATABASE_URL=postgres://localhost/dev

# .env.test
DATABASE_URL=postgres://localhost/test
docker-compose --env-file .env.test up

Networking

Custom Networks

services:
  frontend:
    networks:
      - frontend

  api:
    networks:
      - frontend
      - backend

  database:
    networks:
      - backend

networks:
  frontend:
  backend:

Database not accessible from frontend.

Profiles

Group services for different workflows:

services:
  api:
    # Always starts
    
  frontend:
    profiles: ["full"]
    
  test-runner:
    profiles: ["test"]
# Just API
docker-compose up

# Full stack
docker-compose --profile full up

# Testing
docker-compose --profile test up

Performance Tips

Build Caching

# Order matters for cache
COPY package*.json ./
RUN npm install          # Cached if package.json unchanged
COPY . .                 # Invalidates after this

Parallel Builds

docker-compose build --parallel

Local Development Database

services:
  postgres:
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

Seed data on first start.

Common Issues

Port Conflicts

Error: port 5432 already allocated

Check for local PostgreSQL: lsof -i :5432

Dependency Timing

depends_on waits for container start, not service ready. Use health checks.

Volume Permissions

# In Dockerfile
RUN chown -R node:node /app
USER node

Final Thoughts

Docker Compose isn’t production orchestration—use Kubernetes for that. But for local development, it’s essential.

Start with infrastructure services (databases, queues). Add application services. Use volumes for hot reload. Test the full stack locally before pushing.


Develop locally like you deploy: containerized.

All posts