DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

Docker Compose for Local Development: Complete Setup (2026)

git clonemake dev → working environment with Postgres, Redis, and email testing, same for every developer. That's the goal.

The docker-compose.yml

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules  # use container's node_modules, not host's
      - /app/.next
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
      - REDIS_URL=redis://cache:6379
      - SMTP_HOST=mailhog
      - SMTP_PORT=1025
    env_file:
      - .env.local
    depends_on:
      db:
        condition: service_healthy  # wait for Postgres to actually be ready

  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
      interval: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes

  mailhog:
    image: mailhog/mailhog:latest
    ports:
      - "1025:1025"  # SMTP
      - "8025:8025"  # Web UI — view emails at localhost:8025

volumes:
  postgres_data:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Services communicate by service name: db:5432, cache:6379, mailhog:1025.

Dev Dockerfile with Hot Reload

# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
EXPOSE 3000
CMD ["npm", "run", "dev"]
Enter fullscreen mode Exit fullscreen mode

Source code is mounted via volume — no COPY . needed. Changes on the host are immediately visible inside the container. node_modules from npm ci runs inside the container, the host's version is excluded.

.env Setup

# .env.example (committed — template)
DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
REDIS_URL=redis://cache:6379
SMTP_HOST=mailhog
SMTP_PORT=1025
NEXTAUTH_SECRET=

# .env.local (not committed — real secrets)
NEXTAUTH_SECRET=generate-a-real-secret
STRIPE_SECRET_KEY=sk_test_...
Enter fullscreen mode Exit fullscreen mode

The Makefile

.PHONY: dev down reset db-migrate db-seed shell psql logs

dev:
    docker compose up --build

dev-bg:
    docker compose up --build -d

down:
    docker compose down

reset:
    docker compose down -v && docker compose up --build

db-migrate:
    docker compose exec app npm run db:migrate

db-seed:
    docker compose exec app npm run db:seed

db-reset:
    docker compose exec db psql -U postgres -c "DROP DATABASE IF EXISTS myapp;"
    docker compose exec db psql -U postgres -c "CREATE DATABASE myapp;"
    $(MAKE) db-migrate db-seed

shell:
    docker compose exec app sh

psql:
    docker compose exec db psql -U postgres -d myapp

logs:
    docker compose logs -f

logs-%:
    docker compose logs -f $*

add:
    docker compose exec app npm install $(pkg) && docker compose restart app

status:
    docker compose ps
Enter fullscreen mode Exit fullscreen mode

Performance on Mac

node_modules on a host-mounted volume is slow on macOS. Use named volumes instead:

volumes:
  - .:/app
  - node_modules:/app/node_modules   # named volume — lives in the Linux VM
  - next_cache:/app/.next

volumes:
  node_modules:
  next_cache:
  postgres_data:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Named volumes never cross the VM boundary — npm install and Next.js builds are dramatically faster.

Also enable VirtioFS in Docker Desktop: Settings → General → Use VirtioFS (macOS Ventura+).

Health Checks

db:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
    interval: 5s
    timeout: 5s
    retries: 5

app:
  depends_on:
    db:
      condition: service_healthy  # app waits until Postgres accepts connections
Enter fullscreen mode Exit fullscreen mode

Without this, Next.js tries to connect to Postgres before it's initialized, crashes, and you wonder why.

CI/CD with the Same Stack

# .github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Start services
        run: docker compose up -d db cache

      - name: Wait for Postgres
        run: until docker compose exec -T db pg_isready -U postgres; do sleep 1; done

      - run: npm ci
      - run: npm run db:migrate
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/myapp
      - run: npm test
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/myapp
          REDIS_URL: redis://localhost:6379
Enter fullscreen mode Exit fullscreen mode

App runs directly in CI (faster), but uses the same Docker images for Postgres and Redis.

New Developer Onboarding

git clone https://github.com/org/myapp && cd myapp
cp .env.example .env.local
# Fill in any real API keys

make dev        # starts everything
make db-migrate # in another terminal
make db-seed

open http://localhost:3000  # app
open http://localhost:8025  # emails (MailHog UI)
Enter fullscreen mode Exit fullscreen mode

MailHog catches all outgoing emails — registration, password reset, notifications — without real SMTP or accidentally emailing users.

.dockerignore

node_modules
.next
.git
.env*.local
*.log
coverage
Enter fullscreen mode Exit fullscreen mode

Full article at stacknotice.com/blog/docker-compose-local-dev-2026

Top comments (0)