Local Development with Docker Compose: Databases, Queues, and Services
Local Development with Docker Compose: Databases, Queues, and Services
Running PostgreSQL, Redis, and a message queue locally used to mean installing each service, managing versions, and dealing with conflicts. Docker Compose makes this trivial: define your services in a YAML file, run docker compose up, and everything starts with the right versions and configuration.
This guide provides production-tested Compose configurations for the services developers most commonly need.
The Base Pattern
# docker-compose.yml
name: my-project
services:
postgres:
image: postgres:17
ports:
- "5432:5432"
environment:
POSTGRES_DB: myapp
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev -d myapp"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
Key patterns:
- Named volumes persist data across restarts. Without them, your database is wiped every time you run
docker compose down. - Health checks let dependent services wait until a service is actually ready, not just started.
- Init scripts in
/docker-entrypoint-initdb.d/run on first database creation.
PostgreSQL with Extensions
services:
postgres:
image: postgres:17
ports:
- "5432:5432"
environment:
POSTGRES_DB: myapp
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/01-init.sql
command: >
postgres
-c shared_preload_libraries=pg_stat_statements
-c log_min_duration_statement=100
-c log_statement=none
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev -d myapp"]
interval: 5s
timeout: 5s
retries: 5
-- docker/postgres/init.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Trigram text search
The log_min_duration_statement=100 setting logs any query taking longer than 100ms — useful for catching slow queries during development.
Redis with Persistence
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
--appendonly yes enables persistence so your cache survives restarts. allkeys-lru evicts the least recently used keys when memory is full — sensible defaults for development.
Message Queues
RabbitMQ
services:
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
environment:
RABBITMQ_DEFAULT_USER: dev
RABBITMQ_DEFAULT_PASS: dev
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 10s
retries: 5
The management UI at http://localhost:15672 lets you inspect queues, publish test messages, and monitor consumers. Login with dev/dev.
Object Storage (S3-Compatible)
MinIO
MinIO provides an S3-compatible API locally. Use it instead of connecting to real AWS S3 during development.
services:
minio:
image: minio/minio
ports:
- "9000:9000" # S3 API
- "9001:9001" # Console UI
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- minio_data:/data
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 10s
timeout: 5s
retries: 5
Configure your AWS SDK to use MinIO:
import { S3Client } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: 'http://localhost:9000',
region: 'us-east-1',
credentials: {
accessKeyId: 'minioadmin',
secretAccessKey: 'minioadmin',
},
forcePathStyle: true, // Required for MinIO
});
Search
Elasticsearch
services:
elasticsearch:
image: elasticsearch:8.14.0
ports:
- "9200:9200"
environment:
discovery.type: single-node
xpack.security.enabled: "false"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
volumes:
- es_data:/usr/share/elasticsearch/data
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -q '\"status\":\"green\\|yellow\"'"]
interval: 10s
timeout: 10s
retries: 10
# Optional: Kibana for browsing data
kibana:
image: kibana:8.14.0
ports:
- "5601:5601"
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
depends_on:
elasticsearch:
condition: service_healthy
Meilisearch (Lighter Alternative)
If you need full-text search without Elasticsearch's resource overhead:
services:
meilisearch:
image: getmeili/meilisearch:latest
ports:
- "7700:7700"
environment:
MEILI_ENV: development
volumes:
- meili_data:/meili_data
Meilisearch uses 50MB of RAM vs Elasticsearch's 500MB+. For development, it's usually sufficient.
Useful Commands
# Start all services in background
docker compose up -d
# Start specific services
docker compose up -d postgres redis
# View logs (follow mode)
docker compose logs -f postgres
# Stop all services (data persists in volumes)
docker compose stop
# Stop and remove containers (data persists in volumes)
docker compose down
# Stop and remove containers AND volumes (data is deleted)
docker compose down -v
# Restart a specific service
docker compose restart postgres
# Run a one-off command in a service container
docker compose exec postgres psql -U dev -d myapp
# Check service health
docker compose ps
Tips
Use profiles for optional services
services:
postgres:
image: postgres:17
# Always starts
elasticsearch:
image: elasticsearch:8.14.0
profiles: [search]
# Only starts with: docker compose --profile search up
Override files for personal settings
# docker-compose.override.yml (gitignored)
services:
postgres:
ports:
- "15432:5432" # I use a different port locally
Docker Compose automatically merges docker-compose.yml with docker-compose.override.yml. Add the override file to .gitignore so each developer can customize ports or resources without affecting the shared config.
Wait for dependencies
services:
app:
build: .
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
depends_on with condition: service_healthy waits until the health check passes before starting the dependent service. Without this, your app might start before the database is ready to accept connections.
Resource limits
services:
elasticsearch:
image: elasticsearch:8.14.0
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
Set memory limits so a runaway service doesn't consume all your RAM during development.