Docker
Levantá el stack completo (Postgres, Redis, server, client) con un solo comando para desarrollo local o builds production-like.
El repo trae dos archivos Compose. Elegí el que coincida con lo que necesitás:
| Archivo | Propósito | Qué incluye |
|---|---|---|
docker-compose.dev.yml | Desarrollo local. Hot reload, source mounted, secrets efímeros. | Postgres 16 · Redis 7 · server (Bun + --hot) · client (next dev) |
docker-compose.yml | Build production-like. Imágenes standalone, sin source mount, sin DB. | imagen del server · imagen del client (vos traés tu Postgres + Redis) |
Si querés probar el boilerplate end-to-end sin provisionar nada, usá
docker-compose.dev.yml— levanta el stack completo.
Quick start (desarrollo)
Desde la raíz del repo:
docker compose -f docker-compose.dev.yml upEse único comando no buildea nada — pulla imágenes públicas y montea tu working tree. Esperá ~30 segundos para que bun install termine adentro de ambos containers.
Obtenés:
| Servicio | URL / Port | Notas |
|---|---|---|
| Client | http://localhost:3004 | Next.js con hot reload |
| Server | http://localhost:3005 | Express en Bun, hot reload vía bun --hot |
| Postgres | postgresql://postgres:postgres@localhost:5432/boilerplate | Usuario/pass/db son seedeados por el archivo compose |
| Redis | redis://localhost:6379 | Usado por BullMQ + store de rate-limit |
El server lee su config desde variables de entorno horneadas en el archivo compose — no se necesita .env para arrancar.
[!NOTE] El container del client necesita dos URLs de la API en compose:
NEXT_PUBLIC_API_URL=http://localhost:3005para el browser, yINTERNAL_API_URL=http://server:3005para los fetches del middleware / RSC de Next.js que corren dentro del container. Setups single-host (Vercel, bare-metal) solo necesitanNEXT_PUBLIC_API_URL— el middleware cae a esa cuandoINTERNAL_API_URLno está seteada.
Primer run — aplicar migrations
El dev compose aplica las migrations pendientes automáticamente en el boot — el command del server corre prisma migrate deploy antes de arrancar, así que un checkout fresco levanta con el schema al día. No hace falta ningún paso manual en el primer run.
Cuando cambiás schema.prisma durante desarrollo, creá una migration nueva con migrate dev:
docker compose -f docker-compose.dev.yml exec server \
bunx --cwd apps/server prisma migrate devPrimer run — seed de roles + permissions
El boilerplate trae un script de seed que crea los roles default owner / admin / member para el módulo IAM. Corrélo una vez:
docker compose -f docker-compose.dev.yml exec server \
bun apps/server/prisma/seed.tsListo. Registrate en http://localhost:3004/register y vas a aterrizar en el dashboard.
Comandos comunes
# Tail de logs del server (el más útil)
docker compose -f docker-compose.dev.yml logs -f server
# Abrir un shell dentro del container del server (debug Prisma, correr scripts, etc.)
docker compose -f docker-compose.dev.yml exec server sh
# Conectarse a Postgres con psql
docker compose -f docker-compose.dev.yml exec postgres \
psql -U postgres -d boilerplate
# Parar todo (los containers quedan — `up` la próxima vez es más rápido)
docker compose -f docker-compose.dev.yml stop
# Bajar todo INCLUYENDO el volume de Postgres (destruye data)
docker compose -f docker-compose.dev.yml down -vServicios opcionales
El compose de dev mantiene la surface chica intencionalmente. Si necesitás los adapters opcionales que vienen con el boilerplate, agregalos vos mismo:
- BullMQ Bull Board — arranca dentro del container del server automáticamente cuando
REDIS_URLestá seteado; visitá http://localhost:3005/admin/queues. - Mailpit / MailHog para capturar emails de dev — agregá un service block, después seteá
EMAIL_FROM=dev@localy (si usás Resend) dejáRESEND_API_KEYsin definir para que elLogEmailProvidercaiga. - MongoDB — el archivo compose tiene un block
mongodbcomentado. Descomentalo, seguí ADR-002 para cambiar el provider de Prisma, y actualizáDATABASE_URL.
Build production-like
El otro archivo compose (docker-compose.yml) buildea imágenes inmutables de server y client — útil para verificar que tu código pasa el mismo build pipeline que CI usa, o para pushear a un registry.
# Provisioná tu propio Postgres + Redis primero, después exportá sus URLs:
export DATABASE_URL=postgresql://...
export REDIS_URL=redis://...
export BETTER_AUTH_SECRET=$(openssl rand -hex 32)
docker compose up --buildEsto no incluye una database — el compose de prod asume que estás corriendo contra Postgres managed (Supabase, Neon, RDS, etc.) y Redis managed (Upstash, ElastiCache, etc.). Ver la guía de Supabase para un setup de un solo vendor.
La imagen del server (el stage runner) corre prisma migrate deploy en el boot, antes de arrancar el server HTTP, así que las migrations se aplican automáticamente contra el DATABASE_URL configurado. Si una migration falla, el boot se aborta en lugar de servir contra un schema que el código ya no matchea. El stage worker no migra, así que un solo proceso es dueño del rollout del schema.
Build targets: server vs. worker
apps/server/Dockerfile expone dos targets de runtime que comparten el mismo build:
| Target | Proceso | Healthcheck | Usar cuando |
|---|---|---|---|
runner | HTTP server (dist/index.js) — default | probe HTTP a /health | La API. Un build sin --target resuelve acá. |
worker | worker BullMQ (src/bootstrap/worker.ts) | ninguno | El proceso de background-jobs (emails, webhooks, deletions). |
El worker BullMQ no corre ningún HTTP server, así que no debe heredar el healthcheck HTTP del server — un orquestador (Docker Swarm, Dokploy) probaría /health, nunca obtendría respuesta, marcaría la task como unhealthy y la reciclaría cada ~70 segundos. El target worker setea HEALTHCHECK NONE justamente por esto.
Buildealo explícitamente:
docker build --target worker -f apps/server/Dockerfile -t myapp-worker .[!NOTE] En plataformas que deployan desde un Dockerfile (Dokploy, Railway), apuntá el Build Stage / target del servicio worker a
worker. Si lo dejás vacío buildearunner(el último stage por default) y el worker entra en crash-loop por el healthcheck HTTP heredado.
Troubleshooting
port is already allocated — algo en tu host está usando 3004, 3005, 5432, o 6379. O paralo o remapeá el port host-side en el archivo compose ("3104:3004").
El server arranca pero cada request da 500 con "Cannot find module '@prisma/client'" — el cliente de Prisma no se generó. El container de dev corre bun install en el boot que dispara el postinstall hook; si compitió con la primera request, reiniciá el server: docker compose -f docker-compose.dev.yml restart server.
El hot reload no levanta cambios en macOS — el filesystem polling de Docker Desktop es lento. El archivo compose montea todo el repo con volumes: - .:/app; si ves lag, cambiá Docker Desktop a VirtioFS (Settings → General → Virtual Machine Manager).
Las migrations fallan con "database does not exist" — el healthcheck de Postgres reporta ready antes de que la database boilerplate se cree en el primer boot. Esperá 5 segundos y reintentá el comando migrate.
bun install corre cada vez y es lento — eso es esperado: el compose montea un volume para node_modules para que persista entre restarts, pero el primer install paga el costo completo de red. Boots subsiguientes reusan el volume.
Un container desapareció a mitad de corrida y volvió solo — server, worker y client llevan restart: unless-stopped en el compose de dev. Es intencional: suites E2E pesadas pueden OOM-killear el proceso de Bun bajo flujos de auth concurrentes + encolados de BullMQ, y el auto-restart te ahorra correr docker start a mano. Si un container reinicia repetidamente, mirá los logs (logs -f <service>) — tenés un crash real, no un one-off.