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

125 lines
7.4 KiB
Markdown

# 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:
```bash
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/`](../db/provision/README.md) 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)
```bash
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)