Panorama del despliegue
Qué piezas hay, cómo se conectan y qué hace falta antes de empezar.
Para el paso a paso en la VPS, ver DOKPLOY.md. Este documento explica cómo se compila y arranca la aplicación, y qué necesita para funcionar.
Flujo
flowchart LR
A[git push a main] --> B[Dokploy detecta el cambio]
B --> C[docker build]
C --> D[Arranca el contenedor]
D --> E[prisma migrate deploy]
E --> F{¿Migraciones OK?}
F -->|Sí| G[node server.js]
F -->|No| H[El contenedor no arranca<br/>Dokploy conserva la versión anterior]
G --> I[/api/health]
I -->|sano| J[Traefik enruta el tráfico]Que el contenedor no arranque si fallan las migraciones es deliberado: es preferible seguir sirviendo la versión anterior a servir la nueva contra un esquema incompleto.
La imagen
Dockerfile multi-etapa:
| Etapa | Qué hace |
|---|---|
base | Node 22 Alpine con pnpm y libc6-compat para binarios nativos |
deps | pnpm install --frozen-lockfile |
builder | prisma generate y next build |
migrator | Instala la CLI de Prisma con npm, en árbol plano (DEC-017) |
runner | Sólo lo necesario para ejecutar. Usuario sin privilegios |
Resultado: ~640 MB. La mayor parte es la CLI de Prisma, necesaria para aplicar las migraciones al arrancar (ver DECISIONS.md DEC-017).
Detalles que importan:
output: "standalone"deja en.next/standaloneun servidor con sólo las dependencias de ejecución.- Corre como
nextjs(uid 1001), no como root. TZ=UTCen el contenedor a propósito: la zona del negocio se aplica de forma explícita en el código, nunca por ambiente.- El
HEALTHCHECKconsulta/api/health, que verifica proceso y base de datos. - El build no necesita secretos: la validación del entorno es perezosa.
Variables de entorno
Obligatorias
| Variable | Ejemplo | Notas |
|---|---|---|
DATABASE_URL | postgresql://user:pass@db:5432/royalclean?schema=public | Host interno de Dokploy, nunca público |
AUTH_SECRET | 48 bytes aleatorios | openssl rand -base64 48 |
APP_URL | https://crm.royalcleancr.com | URL pública canónica |
Almacenamiento de evidencia
| Variable | Notas |
|---|---|
R2_ACCOUNT_ID | Id de la cuenta de Cloudflare |
R2_ACCESS_KEY_ID | Token de API de R2 |
R2_SECRET_ACCESS_KEY | Secreto del token |
R2_BUCKET_NAME | royal-clean-evidence |
R2_ENDPOINT | Opcional; se deriva de R2_ACCOUNT_ID si se omite |
Sin estas variables la aplicación arranca igual, pero las funciones de evidencia devuelven un error claro en lugar de romper el arranque.
Opcionales
| Variable | Para qué |
|---|---|
JOBS_SECRET | Token del endpoint /api/jobs/run |
BUSINESS_TIMEZONE | Por defecto America/Costa_Rica |
LOG_LEVEL | debug, info, warn, error |
RESEND_API_KEY | Reservado; sin integración activa |
NODE_ENV=production lo fija la propia imagen.
Migraciones
En producción siempre prisma migrate deploy. Nunca migrate dev, que
puede reescribir el historial.
Lo ejecuta docker/entrypoint.sh antes de levantar el
servidor.
Migraciones compatibles
Para no romper durante el despliegue, cuando la versión anterior y la nueva conviven unos segundos:
- Una columna nueva obligatoria necesita valor por defecto, o se añade en dos pasos: primero opcional, se rellena, luego se hace obligatoria.
- No se renombran columnas en uso: se añade la nueva, se migran los datos, y la antigua se retira en un despliegue posterior.
- Los índices sobre tablas grandes se crean con
CONCURRENTLYen una migración manual.
Rollback
Prisma no revierte migraciones automáticamente. Ante un problema:
- Volver a la versión anterior de la aplicación desde Dokploy. Si la migración era compatible, la versión anterior sigue funcionando.
- Si la migración era destructiva, restaurar el respaldo previo (ver OPERATIONS.md).
- Escribir una migración correctiva hacia adelante y desplegarla.
Por eso las migraciones destructivas se evitan salvo necesidad real.
Antes de dar por bueno un despliegue
-
pnpm verifyen verde -
pnpm test:integrationen verde - La imagen se construye
-
/api/healthresponde{"status":"ok","database":"ok"} - Se puede iniciar sesión
-
/api/jobs/runsin token responde 401 - El bucket de R2 es privado
- PostgreSQL no está expuesto a Internet
- Ningún secreto en el repositorio
- HTTPS activo y renovación automática configurada