Skip to content
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

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Local Microservices Development with Docker Compose

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

FAQ

Q: How is this different from Kubernetes? A: Docker Compose is for single-host local development. Kubernetes orchestrates multi-node production clusters.

Q: Can I use Docker Compose in CI/CD? A: Yes, for integration tests. Use docker-compose -f docker-compose.yml -f docker-compose.ci.yml up to override settings for CI environments.

Is this solution production-ready?

Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.

What are the performance characteristics?

Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.

How do I debug issues with this approach?

Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language’s debugger to step through edge cases.

Multi-Environment Override Files

# docker-compose.test.yml
services:
  api:
    build:
      dockerfile: Dockerfile.test
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgres://postgres:secret@db:5432/app_test
    command: ["npm", "run", "test:integration"]

  db:
    environment:
      POSTGRES_DB: app_test

  # Remove nginx and worker for tests
  nginx:
    profiles:
      - never
  worker:
    profiles:
      - never
# Run integration tests
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from api

Resource Limits and Quotas

# docker-compose.yml (add to each service)
services:
  api:
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

  db:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G

  worker:
    deploy:
      resources:
        limits:
          cpus: '0.25'
          memory: 256M

Seed Data Script

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

set -e

echo "Resetting database..."
docker-compose exec -T db psql -U postgres -d app -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"

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

echo "Seeding users..."
docker-compose exec -T db psql -U postgres -d app <<'SQL'
INSERT INTO users (email, name, created_at) VALUES
  ('alice@example.com', 'Alice', NOW()),
  ('bob@example.com', 'Bob', NOW()),
  ('admin@example.com', 'Admin', NOW());
SQL

echo "Seeding products..."
docker-compose exec -T db psql -U postgres -d app <<'SQL'
INSERT INTO products (name, price, stock) VALUES
  ('Widget', 9.99, 100),
  ('Gadget', 19.99, 50),
  ('Doohickey', 4.99, 200);
SQL

echo "Seed complete!"

Makefile for Common Commands

# Makefile
.PHONY: up down restart logs ps clean rebuild seed test

up:
	docker-compose up -d

down:
	docker-compose down

restart:
	docker-compose restart

logs:
	docker-compose logs -f --tail=100

ps:
	docker-compose ps

clean:
	docker-compose down -v --remove-orphans

rebuild:
	docker-compose up -d --build --force-recreate

seed:
	./seed-dev.sh

test:
	docker-compose -f docker-compose.yml -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from api

shell:
	docker-compose exec api /bin/sh

db-shell:
	docker-compose exec db psql -U postgres -d app

Debugging with Docker Compose

# View logs for a single service
docker-compose logs -f api

# Execute command in running container
docker-compose exec api npm install express

# Inspect container resource usage
docker stats $(docker-compose ps -q)

# Check service health
docker-compose ps
# NAME                COMMAND             SERVICE    STATUS           PORTS
# docker-compose-1   "npm run dev"       api        Up (healthy)     0.0.0.0:3000->3000/tcp

# Rebuild single service after dependency change
docker-compose up -d --build api

# View network details
docker network inspect docker-compose_backend

Additional Best Practices

  1. Use named volumes for persistent data. Anonymous volumes are hard to manage:
# Good: named volume
volumes:
  postgres_data:
    name: myapp_postgres_data
  redis_data:
    name: myapp_redis_data
  1. Pin image versions for reproducibility. Avoid latest:
# Bad
image: postgres:latest

# Good
image: postgres:16.4-alpine
  1. Use .dockerignore to reduce build context:
# .dockerignore
node_modules
.git
.env
coverage
*.log

Additional Common Mistakes

  1. Exposing database ports in production. Only expose during local dev:
# Local dev: OK
ports:
  - "5432:5432"

# Production: remove ports, use internal network only
# ports: []  # No external access
  1. Not using --remove-orphans when switching configs. Orphaned containers linger:
# Clean up orphans
docker-compose down --remove-orphans
  1. Forgetting to set restart policies. Services crash and stay down:
services:
  api:
    restart: unless-stopped
  db:
    restart: unless-stopped
  worker:
    restart: unless-stopped

Additional FAQ

How do I share a Compose setup with my team?

Commit docker-compose.yml and docker-compose.override.yml.example to git. Each developer copies the example to docker-compose.override.yml and customizes locally. The override file is gitignored.

How do I run multiple projects on the same machine?

Use project names to isolate:

docker-compose -p project-a up -d
docker-compose -p project-b up -d

Or set COMPOSE_PROJECT_NAME in .env:

COMPOSE_PROJECT_NAME=myapp

How do I profile a service running in Compose?

Use Docker’s built-in stats or attach a profiler:

# Real-time resource usage
docker stats $(docker-compose ps -q api)

# Attach Node.js inspector
docker-compose exec api node --inspect=0.0.0.0:9229
# Then connect chrome://inspect

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