Docker
Bring up the full stack (Postgres, Redis, server, client) with one command for local development or production-like builds.
The repo ships two Compose files. Pick the one that matches what you need:
| File | Purpose | What it includes |
|---|---|---|
docker-compose.dev.yml | Local development. Hot reload, source mounted, ephemeral secrets. | Postgres 16 · Redis 7 · server (Bun + --hot) · client (next dev) |
docker-compose.yml | Production-like build. Standalone images, no source mount, no DB. | server image · client image (you bring your own Postgres + Redis) |
If you want to try the boilerplate end to end without provisioning anything, use
docker-compose.dev.yml— it boots the entire stack.
Quick start (development)
From the repo root:
docker compose -f docker-compose.dev.yml upThat single command builds nothing — it pulls public images and mounts your working tree. Wait ~30 seconds for bun install to finish inside both containers.
You get:
| Service | URL / Port | Notes |
|---|---|---|
| Client | http://localhost:3004 | Next.js with hot reload |
| Server | http://localhost:3005 | Express on Bun, hot reload via bun --hot |
| Postgres | postgresql://postgres:postgres@localhost:5432/boilerplate | User/pass/db are seeded by the compose file |
| Redis | redis://localhost:6379 | Used by BullMQ + rate-limit store |
The server reads its config from environment variables baked into the compose file — no .env required to boot.
[!NOTE] The client container needs two API URLs in compose:
NEXT_PUBLIC_API_URL=http://localhost:3005for the browser, andINTERNAL_API_URL=http://server:3005for Next.js middleware / RSC fetches that run inside the container. Single-host setups (Vercel, bare-metal) only needNEXT_PUBLIC_API_URL— the middleware falls back to it whenINTERNAL_API_URLis unset.
First run — apply migrations
The compose file starts the server but does not run Prisma migrations automatically (so you can pick the right strategy for your branch). On the first boot, in a second terminal:
docker compose -f docker-compose.dev.yml exec server \
bunx --cwd apps/server prisma migrate deployFor a fresh schema during development you can use migrate dev instead — that creates a new migration from your current schema.prisma:
docker compose -f docker-compose.dev.yml exec server \
bunx --cwd apps/server prisma migrate devFirst run — seed roles + permissions
The boilerplate ships with a seed script that creates the default owner / admin / member roles for the IAM module. Run it once:
docker compose -f docker-compose.dev.yml exec server \
bun apps/server/prisma/seed.tsYou're done. Sign up at http://localhost:3004/register and you'll land on the dashboard.
Common commands
# Tail server logs (most useful one)
docker compose -f docker-compose.dev.yml logs -f server
# Open a shell inside the server container (debug Prisma, run scripts, etc.)
docker compose -f docker-compose.dev.yml exec server sh
# Connect to Postgres with psql
docker compose -f docker-compose.dev.yml exec postgres \
psql -U postgres -d boilerplate
# Stop everything (containers stay around — `up` next time is faster)
docker compose -f docker-compose.dev.yml stop
# Tear it all down INCLUDING the Postgres volume (destroys data)
docker compose -f docker-compose.dev.yml down -vOptional services
The dev compose intentionally keeps the surface small. If you need the optional adapters that ship with the boilerplate, add them yourself:
- BullMQ Bull Board — boots inside the server container automatically when
REDIS_URLis set; visit http://localhost:3005/admin/queues. - Mailpit / MailHog for catching dev emails — add a service block, then set
EMAIL_FROM=dev@localand (if using Resend) leaveRESEND_API_KEYunset so theLogEmailProviderfalls through. - MongoDB — the compose file has a commented-out
mongodbblock. Uncomment it, follow ADR-002 to switch the Prisma provider, and updateDATABASE_URL.
Production-like build
The other compose file (docker-compose.yml) builds immutable server and client images — useful for verifying that your code passes the same build pipeline CI uses, or for pushing to a registry.
# Provision your own Postgres + Redis first, then export their URLs:
export DATABASE_URL=postgresql://...
export REDIS_URL=redis://...
export BETTER_AUTH_SECRET=$(openssl rand -hex 32)
docker compose up --buildThis does not include a database — the production compose assumes you're running against managed Postgres (Supabase, Neon, RDS, etc.) and managed Redis (Upstash, ElastiCache, etc.). See the Supabase guide for a one-vendor setup.
The server image runs prisma migrate deploy at boot, so migrations apply automatically against the configured DATABASE_URL.
Troubleshooting
port is already allocated — something on your host is using 3004, 3005, 5432, or 6379. Either stop it or remap the host-side port in the compose file ("3104:3004").
Server boots but every request 500s with "Cannot find module '@prisma/client'" — Prisma client wasn't generated. The dev container runs bun install on boot which triggers the postinstall hook; if it raced the first request, restart the server: docker compose -f docker-compose.dev.yml restart server.
Hot reload doesn't pick up changes on macOS — Docker Desktop's filesystem polling is slow. The compose file mounts the whole repo with volumes: - .:/app; if you see lag, switch Docker Desktop to VirtioFS (Settings → General → Virtual Machine Manager).
Migrations fail with "database does not exist" — the Postgres healthcheck reports ready before the boilerplate database is created on the very first boot. Wait 5 seconds and retry the migrate command.
bun install runs every time and is slow — that's expected: the compose mounts a volume for node_modules so it persists across restarts, but the first install pays full network cost. Subsequent boots reuse the volume.
A container disappeared mid-run and came back — server, worker, and client all set restart: unless-stopped in the dev compose. This is intentional: heavy E2E suites can OOM-kill the Bun process under concurrent auth flows + BullMQ enqueues, and auto-restart spares you from running docker start by hand. If a container restarts repeatedly, follow the logs (logs -f <service>) — you have a real crash, not a one-off.