panels-origin/db/backups/README.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

108 lines
4.8 KiB
Markdown

# Backups y continuidad (Fase 6)
## 0. Validar antes de diseñar el resto
Coolify despliega Postgres como un recurso "managed" que en realidad es un
contenedor `postgres:16` estándar con volumen persistente. **Hay que
confirmar, para el ambiente real, si se puede:**
1. Montar un `postgresql.conf` custom (para `archive_mode`, `archive_command`, `wal_level`), y
2. Ejecutar un sidecar/proceso adicional (`pgbackrest`) con acceso al mismo volumen de datos y a WAL.
- **Si sí se puede**: seguir esta guía con pgBackRest (PITR real).
- **Si no se puede** (Coolify no expone esos hooks en el plan/versión
usada): el respaldo queda limitado a los `pg_dump` programados que
Coolify ya ofrece -- el RPO pasa a ser "la frecuencia del dump", no
segundos. En ese caso, para producción, evaluar auto-hospedar Postgres
(contenedor propio, fuera del recurso "managed" de Coolify) solo para
tener control de WAL -- es la única forma de bajar el RPO por debajo de
la frecuencia del dump.
Esta decisión condiciona todo lo demás; no asumir que WAL archiving va a
funcionar sin probarlo primero contra el Coolify real del ambiente.
## 1. Qué se respalda y con qué política
| Qué | Herramienta | Frecuencia | Retención | RPO objetivo |
|---|---|---|---|---|
| `panels_platform` (prod) | pgBackRest (o `pg_dump` si no hay WAL) | full diario + diferencial c/6h + WAL continuo | 30 días | segundos (con WAL) / 24h (solo dump) |
| `panels_product` (prod) | igual que arriba | igual | 30 días | igual |
| `panels_platform`/`panels_product` (staging/dev) | `pg_dump` | full diario | 7 días | 24h |
| Archivos (Contabo, Fase 4c) | versionado de objetos del bucket + backup del bucket | continuo (versionado) | igual que la BD | ver nota de coordinación abajo |
| Redis | RDB snapshot | al reiniciar/periódico | ninguna (no es fuente de verdad) | N/A -- ver `db/backups/redis-persistence.md` |
**Respaldar SIEMPRE ambas bases.** Es fácil configurar el backup de
`panels_product` (donde está "todo el negocio") y olvidar `panels_platform`
(que tiene el registro de TODOS los tenants) -- el día del incidente ahí es
cuando se descubre.
## 2. pgBackRest (si Coolify lo permite)
Ver [`pgbackrest-panels_platform.conf`](./pgbackrest-panels_platform.conf) y
[`pgbackrest-panels_product.conf`](./pgbackrest-panels_product.conf) --
plantillas, una `stanza` por base de datos (no por esquema: `panels_product`
es una sola stanza aunque tenga los esquemas iam/core adentro).
Pasos (por base):
```bash
# 1. postgresql.conf del contenedor
wal_level = replica
archive_mode = on
archive_command = 'pgbackrest --stanza=panels_platform archive-push %p'
archive_timeout = 60 # cap de RPO a 60s incluso en bases con poca escritura
# 2. Crear la stanza una vez
pgbackrest --stanza=panels_platform --log-level-console=info stanza-create
# 3. Verificar que el archiving realmente funciona ANTES de confiar en él
pgbackrest --stanza=panels_platform check
# 4. Backups programados (cron / scheduler de Coolify)
pgbackrest --stanza=panels_platform --type=full backup # 1x/semana
pgbackrest --stanza=panels_platform --type=diff backup # cada 6h
```
Repetir con `--stanza=panels_product` y su propio `archive_command`.
### Point-in-time recovery
```bash
pgbackrest --stanza=panels_platform --type=time \
--target="2026-09-01 14:00:00-06" --target-action=promote restore
```
## 3. Si NO hay WAL archiving (solo pg_dump)
```bash
pg_dump --format=custom --file=panels_platform-$(date +%Y%m%d).dump "$DATABASE_URL_PLATFORM_OWNER"
pg_dump --format=custom --file=panels_product-$(date +%Y%m%d).dump "$DATABASE_URL_CORE_OWNER"
```
`--format=custom` permite restore paralelo (`pg_restore -j4`) y restore
selectivo por tabla si algún día hace falta. Subir los `.dump` a un bucket
S3-compatible (puede ser el mismo de Contabo, con prefijo/bucket distinto
al de documentos) con retención por lifecycle policy del bucket.
## 4. Coordinación con el respaldo de archivos (Fase 4c)
Un restore de Postgres a un punto en el tiempo, sin restaurar los archivos
de Contabo al MISMO punto, deja filas (`documents.storage_name`) apuntando
a objetos que ya no existen o que cambiaron. Política:
- Activar versionado de objetos en el bucket de Contabo.
- Si se hace un PITR de Postgres a una hora `T`, documentar que los
archivos subidos/borrados después de `T` pueden quedar huérfanos o
inconsistentes -- es una ventana de inconsistencia aceptada, no un bug
a corregir en caliente durante el incidente.
## 5. Redis
Ver [`redis-persistence.md`](./redis-persistence.md). Resumen: solo RDB
para evitar deslogueo masivo, sin retención de negocio, fuera del alcance
del simulacro de restauración de abajo.
## 6. Simulacro de restauración (mensual, obligatorio)
Ver [`restore-drill-checklist.md`](./restore-drill-checklist.md). Un
backup que nunca se probó restaurar no es un backup, es una esperanza.