panels-origin/docs/coolify.md
Cursor Agent d36c287a82
db/infra: ETL de datos, backups y limpieza de despliegue (fase 5-7)
Fase 5 (migracion de datos):
- api/scripts/migrate-sqlite-to-postgres.ts: ETL unico SQLite -> Postgres.
  Pre-flight (CURP/RFC duplicados case-insensitive, FKs huerfanas,
  tenant_id invalido, fechas mal formateadas) -> carga por base/esquema
  con credenciales _owner (setval de secuencias, OVERRIDING SYSTEM VALUE,
  ON CONFLICT DO NOTHING idempotente) -> verificacion de conteos y sumas
  de dinero con tolerancia. Probado end-to-end contra un dataset SQLite
  legacy sintetico y un Postgres limpio (solo schema+catalogos, sin
  contexto dev): 100% de las filas migradas, sumas de dinero exactas,
  snapshot de uploaded_by/created_by resuelto correctamente.
- db/RUNBOOK-corte.md: checklist go/no-go para el corte por ambiente.
- Fix de bug real encontrado al probar: sin --context-filter explicito,
  Liquibase corre TAMBIEN los changesets context=dev (comportamiento por
  defecto, no "modo seguro") -- documentado en db/README.md con el
  ejemplo correcto (--context-filter='!dev' para staging/produccion).

Fase 5b: patron de compensacion para createTenant ya resuelto en el
rewrite de saas.ts (fase 3/4).

Fase 6 (backups): db/backups/ con plantillas pgBackRest por base,
politica de retencion, nota de persistencia minima de Redis (RDB, sin
retencion de negocio), checklist de simulacro de restauracion mensual,
y la validacion pendiente de si Coolify permite WAL archiving antes de
comprometerse a PITR real.

Fase 7 (limpieza y CI):
- Dockerfile.api simplificado (sin JRE/Liquibase/FFI). Dockerfile.migrate
  y Dockerfile.provision nuevos, de un solo uso, para el paso explicito
  de deploy (nunca sirven trafico).
- docker-compose.yml: postgres+redis+provision+migrate para dev local
  end-to-end; api ya no arranca hasta que migrate termina bien.
- docs/coolify.md y .env.example actualizados al modelo de 2 bases +
  Redis + Contabo.
- api/schema.sql eliminado (desincronizado, competia con Liquibase como
  fuente de verdad).
- .github/workflows/ci.yml: Postgres+Redis de servicio, aprovisiona
  roles/ACLs, dry-run de Liquibase (updateSQL) antes de aplicar,
  verify-isolation.sh, deno check + deno test, build de los dos frontends.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-02 21:05:07 +00:00

7.4 KiB

Deploy PANELS en Coolify (Docker Compose + Postgres + Redis)

Resumen

Qué Cuánto
Postgres gestionado en Coolify 1 instancia por ambiente, con 2 bases: panels_platform, panels_product (esquemas iam/core)
Redis gestionado en Coolify 1 instancia por ambiente, con 2 usuarios ACL (panels_iam_redis, panels_core_redis)
Object storage Contabo Object Storage (S3-compatible) para expedientes/PDFs/logos cifrados
Contenedores de la app 3 (api, web-panel, web-saas)
Volumen persistente 1 → /app/data en api (solo fallback local si no hay Contabo configurado -- no usar así en producción)

Los fronts (Alpine + nginx) hacen proxy de /v1 al servicio api, así las cookies de sesión van same-origin.

Ver el plan de migración para el detalle de arquitectura: por qué panels_platform es una base separada, por qué iam/core son esquemas distintos dentro de panels_product, y las reglas del monolito modular.

Datos sensibles (qué NO va al git)

Ítem Estado
.env gitignored — no commitear
data/ (fallback local de archivos, si no hay Contabo) gitignored
SMTP password en SaaS vive en panels_platform.smtp_settings — proteger backups
Defaults de desarrollo en código SESSION_SECRET/DOCS_KEY solo tienen fallback si DENO_ENV no es production -- fuera de eso, la app falla al arrancar si faltan

Variables de entorno (servicio api)

Obligatorias

Variable Formato Uso
SESSION_SECRET string largo aleatorio Firma interna (no ya la cookie -- la sesión vive en Redis, ver Fase 4)
DOCS_KEY 64 caracteres hex (32 bytes) Cifrado AES-GCM de documentos
SEED_PASSWORD string Password inicial usado por api/scripts/bootstrap-admin.ts
PANEL_LOGIN_URL URL absoluta Link en correos de acceso
DATABASE_URL_PLATFORM postgresql://panels_platform_app:...@host:5432/panels_platform Runtime, rol _app
DATABASE_URL_IAM postgresql://panels_iam_app:...@host:5432/panels_product Runtime, rol _app, esquema iam
DATABASE_URL_CORE postgresql://panels_core_app:...@host:5432/panels_product Runtime, rol _app, esquema core
DATABASE_URL_IAM_OWNER igual, rol _owner Solo para el lookup de login por username (bypassa RLS a propósito, ver api/iam_db.ts)
DATABASE_URL_CORE_OWNER igual, rol _owner Solo para resoluciones administrativas puntuales (ver api/db.ts#getCoreDb)
REDIS_URL_IAM redis://panels_iam_redis:...@host:6379 Sesiones
REDIS_URL_CORE redis://panels_core_redis:...@host:6379 Cache

Generar secretos:

openssl rand -hex 32   # SESSION_SECRET o DOCS_KEY
openssl rand -hex 24   # passwords de roles Postgres/Redis

Recomendadas (HTTPS / Coolify)

Variable Default compose Uso
COOKIE_SECURE true Cookie solo por HTTPS
PORT 8000 Puerto interno API
CORS_ORIGINS vacío Lista https://a,https://b si la API se llama cross-origin; con proxy /v1 en nginx no hace falta

Opcionales

Variable Uso
API_KEY Auth alternativa por header X-API-Key + X-Tenant-Id obligatorio (ya no ve todos los tenants, ver revisión de seguridad)
VCARD_BASE Prefijo QR/vCard gafetes
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM Correo por env (alternativa al panel /smtp)
S3_ENDPOINT / S3_BUCKET / S3_REGION / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY Contabo Object Storage (Fase 4c). Sin esto, cae a disco local -- no recomendado en producción

web-panel y web-saas no necesitan variables de entorno en runtime (estáticos + proxy nginx).

Imágenes

  • api: Deno 2.9, sin FFI ni JRE (Fase 2/7 del plan de migración).
  • migrate: JRE 21 + Liquibase -- imagen aparte, de un solo uso, NO sirve tráfico (ver Dockerfile.migrate).
  • web-panel / web-saas: build Node Alpine → nginx Alpine.

Archivos: Dockerfile.api, Dockerfile.migrate, Dockerfile.provision, Dockerfile.web-panel, Dockerfile.web-saas, docker-compose.yml, web-panel/nginx.conf, web-saas/nginx.conf.

En Coolify el servicio de PANELS se llama web-panel (FQDN ej. panels.mrdev.mx). La carpeta web/ queda libre para una website futura.

Paso a paso en Coolify

  1. Provisionar Postgres y Redis como recursos gestionados de Coolify para el ambiente (uno de cada, no por módulo).
  2. Aprovisionar roles/esquemas/ACLs: correr los scripts de db/provision/ contra ese Postgres/Redis (una vez, desde tu máquina o un job manual -- Coolify no lo hace por ti).
  3. Aplicar Liquibase (paso explícito, NO ocurre al arrancar la app): ./db/update.sh all --context-filter='!dev' con las credenciales _owner. En staging/producción, nunca olvidar el --context-filter -- sin él, Liquibase corre TAMBIÉN los changesets de demo (context=dev).
  4. Bootstrap del primer admin: deno run ... api/scripts/bootstrap-admin.ts platform y ... tenant --tenant-id=... --company-code=... (ver db/README.md).
  5. Push este repo (sin .env ni data/).
  6. Coolify → New Resource → Docker Compose → docker-compose.yml (solo para api/web-panel/web-saas -- si Postgres/Redis ya son recursos gestionados aparte, quitar esos servicios del compose antes de desplegar, o apuntar sus variables a los recursos gestionados en vez de los contenedores locales del compose).
  7. Persistent storage: volumen panel-data → /app/data en api (solo fallback de archivos si no hay Contabo).
  8. Dominios: web-panel → panels; web-saas → saas; api sin FQDN (proxy /v1).
  9. Cargar en Coolify todas las variables obligatorias de la tabla de arriba + COOKIE_SECURE=true.
  10. Deploy.
  11. Verificar https://app…/v1/health → debe responder {"ok":true,"core":true,"platform":true,"redis":{"iam":true,"core":true}}. Si algo es false, la app ni siquiera debería haber arrancado (fail-fast, Fase 7).
  12. Login SaaS admin / tu SEED_PASSWORD.
  13. SMTP en /smtp o por SMTP_*.

Local (todo en docker-compose, incluyendo Postgres/Redis propios)

cp .env.example .env   # rellenar TODAS las variables (incluye los passwords de roles Postgres/Redis)
docker compose up --build

El compose local incluye: provision (roles/esquemas/ACLs, un solo uso) → migrate (Liquibase con datos de demo, un solo uso) → api/web-panel/web-saas. Para producción, provision/migrate NO se corren así -- ver db/provision/README.md y db/RUNBOOK-corte.md.

Sin las variables obligatorias, compose no arranca (:? en docker-compose.yml).

Checklist ops

  • .env y data/ fuera del git
  • Secrets distintos a los de desarrollo (incluye los 6 roles Postgres + 2 Redis)
  • Backup de panels_platform Y panels_product (ver db/backups/) -- no solo una
  • Backup del bucket de Contabo (documentos cifrados)
  • COOKIE_SECURE=true en HTTPS
  • Healthcheck API OK (/v1/health con las 5 conexiones en true)
  • --context-filter explícito en cada corrida de Liquibase contra staging/producción

Qué no hace falta

  • Contenedor monolítico Node+Deno+Java sirviendo tráfico (Liquibase vive en su propia imagen, de un solo uso)
  • FFI de SQLite (ya no hay SQLite en ningún ambiente)