SaaS Starter

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:

ArchivoPropósitoQué incluye
docker-compose.dev.ymlDesarrollo local. Hot reload, source mounted, secrets efímeros.Postgres 16 · Redis 7 · server (Bun + --hot) · client (next dev)
docker-compose.ymlBuild 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 up

Ese ú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:

ServicioURL / PortNotas
Clienthttp://localhost:3004Next.js con hot reload
Serverhttp://localhost:3005Express en Bun, hot reload vía bun --hot
Postgrespostgresql://postgres:postgres@localhost:5432/boilerplateUsuario/pass/db son seedeados por el archivo compose
Redisredis://localhost:6379Usado 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:3005 para el browser, y INTERNAL_API_URL=http://server:3005 para los fetches del middleware / RSC de Next.js que corren dentro del container. Setups single-host (Vercel, bare-metal) solo necesitan NEXT_PUBLIC_API_URL — el middleware cae a esa cuando INTERNAL_API_URL no 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 dev

Primer 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.ts

Listo. 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 -v

Servicios 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_URL está seteado; visitá http://localhost:3005/admin/queues.
  • Mailpit / MailHog para capturar emails de dev — agregá un service block, después seteá EMAIL_FROM=dev@local y (si usás Resend) dejá RESEND_API_KEY sin definir para que el LogEmailProvider caiga.
  • MongoDB — el archivo compose tiene un block mongodb comentado. 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 --build

Esto 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:

TargetProcesoHealthcheckUsar cuando
runnerHTTP server (dist/index.js) — defaultprobe HTTP a /healthLa API. Un build sin --target resuelve acá.
workerworker BullMQ (src/bootstrap/worker.ts)ningunoEl 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 buildea runner (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.

En esta página