Production-Grade Docker Compose Setup for Local Development
Build a robust local development environment with Docker Compose that mirrors production. Includes hot-reloading, debugging, database persistence, and multi-service orchestration.
Why Docker Compose for Local Development?
"Works on my machine" is no longer an acceptable excuse. Docker Compose solves the environment consistency problem by defining your entire development stack in code. One docker-compose up command gives you:
- Database with seeded data
- Redis cache
- Message queue
- Your application with hot-reloading
- Supporting services (mail catcher, S3 emulator)
All configured identically to production, all started in seconds.
The Complete Stack
Let's build a full-stack development environment for a Node.js API with PostgreSQL, Redis, and background workers.
Project Structure
project/
├── docker-compose.yml
├── docker-compose.override.yml # Local dev overrides
├── .env.example
├── Dockerfile
├── Dockerfile.dev
└── docker/
├── postgres/
│ └── init.sql
├── nginx/
│ └── default.conf
└── redis/
└── redis.conf
Base docker-compose.yml
version: '3.8'
services:
PostgreSQL Database
postgres:
image: postgres:16-alpine
container_name: app_postgres
environment:
POSTGRES_DB: ${DB_NAME:-appdb}
POSTGRES_USER: ${DB_USER:-devuser}
POSTGRES_PASSWORD: ${DB_PASSWORD:-devpass}
POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C'
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-devuser}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app_network
Redis Cache
redis:
image: redis:7-alpine
container_name: app_redis
command: redis-server /usr/local/etc/redis/redis.conf
ports:
- "6379:6379"
volumes:
- redis_data:/data
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- app_network
Main Application
app:
build:
context: .
dockerfile: Dockerfile.dev
args:
NODE_VERSION: 20
container_name: app_server
environment:
NODE_ENV: development
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ${DB_NAME:-appdb}
DB_USER: ${DB_USER:-devuser}
DB_PASSWORD: ${DB_PASSWORD:-devpass}
REDIS_HOST: redis
REDIS_PORT: 6379
ports:
- "3000:3000"
- "9229:9229" # Node debugger
volumes:
# Source code hot-reloading
- ./src:/app/src
- ./package.json:/app/package.json
- ./package-lock.json:/app/package-lock.json
# Avoid overwriting node_modules
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command: npm run dev
networks:
- app_network
Background Worker
worker:
build:
context: .
dockerfile: Dockerfile.dev
container_name: app_worker
environment:
NODE_ENV: development
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ${DB_NAME:-appdb}
DB_USER: ${DB_USER:-devuser}
DB_PASSWORD: ${DB_PASSWORD:-devpass}
REDIS_HOST: redis
REDIS_PORT: 6379
volumes:
- ./src:/app/src
- /app/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command: npm run worker
networks:
- app_network
Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: app_nginx
ports:
- "80:80"
volumes:
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
networks:
- app_network
MailHog (Email testing)
mailhog:
image: mailhog/mailhog:latest
container_name: app_mailhog
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
networks:
- app_network
MinIO (S3-compatible storage)
minio:
image: minio/minio:latest
container_name: app_minio
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001" # Console
volumes:
- minio_data:/data
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
networks:
- app_network
volumes:
postgres_data:
redis_data:
minio_data:
networks:
app_network:
driver: bridge
Dockerfile.dev (Development)
FROM node:20-alpine
Install development tools
RUN apk add --no-cache
git
python3
make
g++
WORKDIR /app
Copy package files
COPY package*.json ./
Install ALL dependencies (including devDependencies)
RUN npm ci
Copy source code
COPY . .
Expose ports
EXPOSE 3000 9229
Default command (can be overridden)
CMD ["npm", "run", "dev"]
docker-compose.override.yml (Local Overrides)
version: '3.8'
This file is automatically merged with docker-compose.yml
Use it for local development customizations
services:
app:
# Enable hot-reloading
environment:
CHOKIDAR_USEPOLLING: 'true' # For Windows/Mac file watching
DEBUG: 'app:*'
# Add local DNS entries
extra_hosts:
- "host.docker.internal:host-gateway"
postgres:
# Expose on different port if 5432 is taken
ports:
- "5433:5432"
Add pgAdmin for database management
pgadmin:
image: dpage/pgadmin4:latest
container_name: app_pgadmin
environment:
PGADMIN_DEFAULT_EMAIL: admin@localhost
PGADMIN_DEFAULT_PASSWORD: admin
PGADMIN_CONFIG_SERVER_MODE: 'False'
ports:
- "5050:80"
networks:
- app_network
Add Redis Commander for Redis management
redis-commander:
image: rediscommander/redis-commander:latest
container_name: app_redis_commander
environment:
REDIS_HOSTS: local:redis:6379
ports:
- "8081:8081"
depends_on:
- redis
networks:
- app_network
Hot-Reloading Configuration
package.json Scripts
{
"scripts": {
"dev": "nodemon --inspect=0.0.0.0:9229 --watch src src/index.js",
"worker": "nodemon --watch src src/worker.js",
"db:migrate": "npx knex migrate:latest",
"db:seed": "npx knex seed:run",
"db:reset": "npx knex migrate:rollback --all && npm run db:migrate && npm run db:seed"
}
}
nodemon.json
{
"watch": ["src"],
"ext": "js,json,yaml",
"ignore": ["src/**/*.test.js", "node_modules"],
"delay": "1000",
"env": {
"NODE_ENV": "development"
},
"execMap": {
"js": "node --inspect=0.0.0.0:9229"
}
}
Database Initialization
docker/postgres/init.sql
-- Create extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- Create application schema
CREATE SCHEMA IF NOT EXISTS app;
-- Grant privileges
GRANT ALL PRIVILEGES ON SCHEMA app TO devuser;
GRANT ALL PRIVILEGES ON DATABASE appdb TO devuser;
-- Create initial tables
CREATE TABLE IF NOT EXISTS app.users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_users_email ON app.users(email);
CREATE INDEX idx_users_created_at ON app.users(created_at DESC);
-- Insert seed data
INSERT INTO app.users (email, password_hash)
VALUES
('admin@example.com', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyB1n5fY3P3u'),
('user@example.com', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyB1n5fY3P3u')
ON CONFLICT (email) DO NOTHING;
Useful Docker Compose Commands
# Start all services
docker-compose up -d
Start specific services
docker-compose up -d postgres redis
View logs
docker-compose logs -f app
docker-compose logs --tail=100 postgres
Execute commands in containers
docker-compose exec app npm run db:migrate
docker-compose exec postgres psql -U devuser -d appdb
Restart specific service
docker-compose restart app
Rebuild after Dockerfile changes
docker-compose up -d --build app
Stop all services
docker-compose down
Stop and remove volumes (fresh start)
docker-compose down -v
View service status
docker-compose ps
Scale workers
docker-compose up -d --scale worker=3
Debugging Setup
VS Code launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Docker: Attach to Node",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "/app/src",
"protocol": "inspector",
"restart": true,
"skipFiles": ["<node_internals>/**"]
}
]
}
Environment Management
.env.example
# Database
DB_NAME=appdb
DB_USER=devuser
DB_PASSWORD=devpass
Redis
REDIS_PASSWORD=
Application
NODE_ENV=development
PORT=3000
JWT_SECRET=dev_secret_change_in_production
Email
SMTP_HOST=mailhog
SMTP_PORT=1025
S3
S3_ENDPOINT=http://minio:9000
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=uploads
Health Checks and Readiness
// src/healthcheck.js
const express = require('express');
const { Pool } = require('pg');
const Redis = require('ioredis');
const router = express.Router();
router.get('/health', async (req, res) => {
const checks = {
status: 'healthy',
timestamp: new Date().toISOString(),
services: {}
};
// Check database
try {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await pool.query('SELECT 1');
checks.services.database = 'healthy';
} catch (error) {
checks.services.database = 'unhealthy';
checks.status = 'degraded';
}
// Check Redis
try {
const redis = new Redis({ host: process.env.REDIS_HOST });
await redis.ping();
checks.services.redis = 'healthy';
} catch (error) {
checks.services.redis = 'unhealthy';
checks.status = 'degraded';
}
const statusCode = checks.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(checks);
});
module.exports = router;
Makefile for Common Tasks
.PHONY: up down logs shell migrate seed test
Start all services
up:
docker-compose up -d
Stop all services
down:
docker-compose down
View logs
logs:
docker-compose logs -f
Access app shell
shell:
docker-compose exec app sh
Run database migrations
migrate:
docker-compose exec app npm run db:migrate
Seed database
seed:
docker-compose exec app npm run db:seed
Reset database
reset:
docker-compose exec app npm run db:reset
Run tests
test:
docker-compose exec app npm test
Fresh start (remove volumes)
fresh:
docker-compose down -v
docker-compose up -d --build
Check service health
health:
curl http://localhost:3000/health
Database shell
db:
docker-compose exec postgres psql -U devuser -d appdb
Performance Optimization
Use BuildKit for Faster Builds
# Enable BuildKit in .env or shell profile
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
Multi-stage caching
docker-compose build --parallel
Volume Performance (Mac/Windows)
# Use :cached for better performance on macOS
volumes:
- ./src:/app/src:cached
- ./node_modules:/app/node_modules:delegated
Production Parity
# docker-compose.prod.yml
version: '3.8'
services:
app:
build:
dockerfile: Dockerfile # Production Dockerfile
environment:
NODE_ENV: production
# No source code volumes - baked into image
# No debug ports
restart: unless-stopped
Conclusion
A well-configured Docker Compose setup eliminates the "it works on my machine" problem and accelerates onboarding. New developers can have a fully functional environment running in minutes, not days.
Key Takeaways
- Use health checks to ensure services start in correct order
- Mount source code for hot-reloading during development
- Separate dev and prod Dockerfiles
- Use override files for local customizations
- Include supporting services (mail, S3) for complete parity
- Configure debugging for IDE integration
- Document commands in Makefile
- Persist data with named volumes
- Use BuildKit for faster builds
Your development environment should be a joy to use, not a source of frustration. Invest time in Docker Compose configuration upfront, and reap the benefits for the entire project lifecycle.
Related Articles
GraphQL API Design - Production Architecture and Best Practices for Scalable Systems
Master GraphQL API design covering schema design principles, resolver optimization, N+1 query prevention with DataLoader, authentication and authorization patterns, caching strategies, error handling, and production deployment for high-performance GraphQL systems.
Testing Strategies - Unit, Integration, and E2E Testing Best Practices for Production Quality
Comprehensive guide to testing strategies covering unit tests, integration tests, end-to-end testing, test-driven development, mocking patterns, testing pyramid, and production testing practices for reliable software delivery.
Monitoring and Observability - Production Systems Performance and Debugging at Scale
Master monitoring and observability covering metrics collection with Prometheus, distributed tracing with OpenTelemetry, log aggregation, alerting strategies, SLOs/SLIs, and production debugging techniques for reliable systems.
Written by StaticBlock Editorial
StaticBlock Editorial is a technical writer and software engineer specializing in web development, performance optimization, and developer tooling.