0% read
Skip to main content
Production-Grade Docker Compose Setup for Local Development

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.

S
StaticBlock Editorial
14 min read

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.

Found this helpful? Share it!

Related Articles

S

Written by StaticBlock Editorial

StaticBlock Editorial is a technical writer and software engineer specializing in web development, performance optimization, and developer tooling.