yaml Containers August 1, 2025

Docker Compose Development Stack

A complete Docker Compose development environment with hot reload, PostgreSQL, Redis, and email testing. One command to start coding.

dockerdocker-composedevelopmentlocal-dev

Description

A batteries-included Docker Compose setup for local development. Includes Node.js app with hot reload, PostgreSQL with health checks, Redis for caching, and Mailpit for email testing.

docker-compose.yml

# docker-compose.yml
services:
  # ════════════════════════════════════════════════════════════
  # Application
  # ════════════════════════════════════════════════════════════
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: myapp-dev
    restart: unless-stopped
    ports:
      - "3000:3000"
      - "9229:9229"  # Node.js debugger
    volumes:
      - .:/app
      - /app/node_modules  # Don't override node_modules
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://dev:devpass@db:5432/myapp
      - REDIS_URL=redis://redis:6379
      - SMTP_HOST=mailpit
      - SMTP_PORT=1025
      - LOG_LEVEL=debug
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: npm run dev

  # ════════════════════════════════════════════════════════════
  # PostgreSQL Database
  # ════════════════════════════════════════════════════════════
  db:
    image: postgres:16-alpine
    container_name: myapp-db
    restart: unless-stopped
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 5s
      timeout: 3s
      retries: 5

  # ════════════════════════════════════════════════════════════
  # Redis Cache
  # ════════════════════════════════════════════════════════════
  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  # ════════════════════════════════════════════════════════════
  # Email Testing (Mailpit)
  # ════════════════════════════════════════════════════════════
  mailpit:
    image: axllent/mailpit:latest
    container_name: myapp-mail
    restart: unless-stopped
    ports:
      - "8025:8025"  # Web UI
      - "1025:1025"  # SMTP
    environment:
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1

  # ════════════════════════════════════════════════════════════
  # Adminer (DB Management UI)
  # ════════════════════════════════════════════════════════════
  adminer:
    image: adminer:latest
    container_name: myapp-adminer
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      ADMINER_DEFAULT_SERVER: db

volumes:
  pgdata:
  redisdata:

Dockerfile.dev

# Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

# Install dependencies for native modules
RUN apk add --no-cache python3 make g++

# Install dependencies first (caching layer)
COPY package*.json ./
RUN npm install

# Copy source (overridden by bind mount in dev)
COPY . .

# Expose app + debugger ports
EXPOSE 3000 9229

# Start with hot reload
CMD ["npm", "run", "dev"]

Environment File

# .env.development
NODE_ENV=development
PORT=3000

# Database
DATABASE_URL=postgresql://dev:devpass@localhost:5432/myapp

# Redis
REDIS_URL=redis://localhost:6379

# Email (Mailpit)
SMTP_HOST=localhost
SMTP_PORT=1025

# Auth
JWT_SECRET=dev-secret-do-not-use-in-production
SESSION_SECRET=another-dev-secret

# External Services (use dev/sandbox keys)
STRIPE_SECRET_KEY=sk_test_xxx
AWS_ACCESS_KEY_ID=your-dev-key

Makefile

# Makefile
.PHONY: dev dev-build dev-down dev-reset dev-logs dev-shell dev-db test

# Start development environment
dev:
	docker compose up

# Start with rebuild
dev-build:
	docker compose up --build

# Stop all services
dev-down:
	docker compose down

# Reset everything (including data volumes)
dev-reset:
	docker compose down -v
	docker compose up --build

# View application logs
dev-logs:
	docker compose logs -f app

# Shell into app container
dev-shell:
	docker compose exec app sh

# Connect to database
dev-db:
	docker compose exec db psql -U dev myapp

# Run tests
test:
	docker compose exec app npm test

# Run database migrations
migrate:
	docker compose exec app npm run migrate

# Seed database
seed:
	docker compose exec app npm run seed

Init SQL Script

-- scripts/init.sql
-- This runs on first container startup

-- Create extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";

-- Create additional schemas
CREATE SCHEMA IF NOT EXISTS audit;

-- Create audit log table
CREATE TABLE IF NOT EXISTS audit.logs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    table_name VARCHAR(100) NOT NULL,
    action VARCHAR(10) NOT NULL,
    old_data JSONB,
    new_data JSONB,
    user_id UUID,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Grant permissions
GRANT ALL PRIVILEGES ON SCHEMA audit TO dev;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA audit TO dev;

Package.json Scripts

{
  "scripts": {
    "dev": "tsx watch --inspect=0.0.0.0:9229 src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "vitest",
    "test:coverage": "vitest --coverage",
    "migrate": "prisma migrate dev",
    "seed": "tsx prisma/seed.ts",
    "lint": "eslint src/",
    "type-check": "tsc --noEmit"
  }
}

Usage

# Start everything
make dev

# Access services:
#   App:     http://localhost:3000
#   Mailpit: http://localhost:8025
#   Adminer: http://localhost:8080
#   DB:      localhost:5432
#   Redis:   localhost:6379

# Run commands
make dev-shell     # Shell into container
make dev-db        # Connect to PostgreSQL
make dev-logs      # View logs
make migrate       # Run migrations
make test          # Run tests

# Reset everything (fresh start)
make dev-reset