git clone → make 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:
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"]
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_...
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
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:
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
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
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)
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
Full article at stacknotice.com/blog/docker-compose-local-dev-2026
Top comments (0)