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

4.8 KiB

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 y 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):

# 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

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)

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. 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. Un backup que nunca se probó restaurar no es un backup, es una esperanza.