Docker Compose — Full Dev Environment Project

3 minute read

Build a complete local development environment with a web app, database, cache, mail catcher, and monitoring — combining every Compose concept from this tutorial.

Architecture

┌──────────────────────────────────────────────────────────────┐
│                    Docker Network: dev-net                     │
│                                                               │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────┐  │
│  │  Web App │ │ Postgres │ │  Redis   │ │   MailHog      │  │
│  │  :3000   │ │  :5432   │ │  :6379   │ │   :1025/:8025  │  │
│  └────┬─────┘ └──────────┘ └──────────┘ └────────────────┘  │
│       │                                                       │
│  Compose Watch                    ┌────────────────────────┐ │
│  (hot reload)    [debug profile]  │  Adminer   │ pgAdmin   │ │
│                                   │  :8080     │  :5050    │ │
│                                   └────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

Project Setup

mkdir fulldev-compose && cd fulldev-compose
mkdir app

Step 1 — Application Code

Create app/package.json:

{
  "name": "fulldev-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "express": "^4.18.0",
    "pg": "^8.11.0",
    "redis": "^4.6.0",
    "nodemailer": "^6.9.0"
  }
}

Create app/server.js:

const express = require('express');
const { Pool } = require('pg');
const { createClient } = require('redis');
const nodemailer = require('nodemailer');

const app = express();
app.use(express.json());

const pool = new Pool({
  host: 'postgres', port: 5432,
  database: 'devdb', user: 'devuser', password: 'devpass'
});

const redis = createClient({ url: 'redis://redis:6379' });
redis.connect().catch(console.error);

const mailer = nodemailer.createTransport({
  host: 'mailhog', port: 1025
});

app.get('/health', async (req, res) => {
  res.json({ status: 'ok', time: new Date().toISOString() });
});

app.post('/notify', async (req, res) => {
  await mailer.sendMail({
    from: 'app@example.com', to: 'user@example.com',
    subject: 'Test Email', text: 'Sent from Compose dev environment!'
  });
  res.json({ sent: true });
});

app.listen(3000, () => console.log('Dev server on :3000'));

Create app/Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

Step 2 — Base Compose File

Create compose.yml:

services:
  app:
    build:
      context: ./app
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      NODE_ENV: development
    networks:
      - dev-net
    develop:
      watch:
        - action: sync
          path: ./app
          target: /app
          ignore:
            - node_modules/
        - action: rebuild
          path: ./app/package.json

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: devdb
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpass
    volumes:
      - pg-data:/var/lib/postgresql/data
    networks:
      - dev-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    networks:
      - dev-net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  mailhog:
    image: mailhog/mailhog:latest
    ports:
      - "1025:1025"
      - "8025:8025"
    networks:
      - dev-net

  # Debug tools — only start with: docker compose --profile debug up
  adminer:
    image: adminer:latest
    profiles:
      - debug
    ports:
      - "8080:8080"
    networks:
      - dev-net

  pgadmin:
    image: dpage/pgadmin4:latest
    profiles:
      - debug
    ports:
      - "5050:80"
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: admin
    networks:
      - dev-net

volumes:
  pg-data:
  redis-data:

networks:
  dev-net:
    driver: bridge

Step 3 — Production Override

Create compose.prod.yml:

services:
  app:
    build:
      context: ./app
    command: ["npm", "start"]
    environment:
      NODE_ENV: production
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256m

  postgres:
    deploy:
      resources:
        limits:
          memory: 512m

  redis:
    deploy:
      resources:
        limits:
          memory: 128m

  mailhog:
    profiles:
      - never

Step 4 — Launch for Development

Start the core services:

docker compose up -d --build

Start with debug tools:

docker compose --profile debug up -d

Use Compose Watch for hot reload:

docker compose watch

Step 5 — Test Everything

Health check:

curl http://localhost:3000/health
{"status":"ok","time":"2024-01-15T12:00:00.000Z"}

Send a test email:

curl -X POST http://localhost:3000/notify
{"sent":true}

View the email in MailHog at http://localhost:8025.

Step 6 — Simulate Production

docker compose -f compose.yml -f compose.prod.yml up -d --build

This merges the production overrides — MailHog is excluded (never profile), resources are limited, and the app runs in production mode.

Enhancements to Try

  1. Add Nginx as a reverse proxy in front of the app
  2. Add a frontend service with React/Vue
  3. Add database migrations as an init container
  4. Add Grafana + Prometheus behind a monitoring profile

Quick Reference

Item Detail
App port 3000:3000
MailHog SMTP 1025:1025
MailHog UI 8025:8025
Adminer 8080:8080 (debug profile)
pgAdmin 5050:80 (debug profile)
Compose Watch docker compose watch
Prod override compose.prod.yml