Saltar al contenido

Respaldos, recuperación a un momento dado y simulacro de restauración

Respalda Quire, restáuralo a un momento dado y comprueba el proceso con un simulacro de restauración.

Ver como Markdown

El diseño está en la sección 8 de docs/architecture/23-ops.md. Este es el manual de operaciones para el producto Docker Compose. Está redactado para que pueda seguirlo alguien que no lo escribió; si algún paso no queda claro, hay que corregir este documento.

Qué se protege y cómo

Recurso Método Ubicación
La base de datos Archivado continuo de WAL, como máximo cada 60 segundos desde el primer inicio Volumen pgwal
La base de datos Respaldos base con pg_basebackup, a diario de forma predeterminada (backup-scheduler) Volumen pgbackup
La base de datos Copias cifradas de respaldos base y WAL cada cinco minutos (backup-offsite) Almacén independiente que indiques
Archivos Volumen files. Cópialo con la herramienta de respaldo del host o usa almacenamiento de objetos con control de versiones Volumen files
Secretos docker/.env, sobre todo QUIRE_MASTER_KEY (y cualquier QUIRE_MASTER_KEY_RETIRED que aún se use), QUIRE_BACKUP_ENCRYPTION_KEY y docker/secrets/audit-signing-key.pem Guarda una copia fuera de este host
Índices de búsqueda, cachés y versiones procesadas No se respaldan; se vuelven a generar

Objetivos: un punto de recuperación a no más de 60 segundos del fallo y una restauración en menos de 60 minutos para una base de datos de 500 GB.

Hay dos errores comunes. Si restauras una base de datos sin sus archivos, las páginas aparecen dañadas. Si la restauras sin QUIRE_MASTER_KEY, no podrás descifrar las credenciales de SSO, webhooks e integraciones que contiene. Hasta que una rotación de la clave maestra termine sin valores pendientes de resolver (key-rotation.md), también se necesitan las claves retiradas. Todo eso forma parte del respaldo.

Crear respaldos

Para crear un respaldo base de todo el clúster:

docker compose -f docker/compose.yaml --profile backup run --rm backup

Conserva los respaldos base más recientes según QUIRE_BACKUP_KEEP (5 de forma predeterminada) y elimina el WAL que ya no necesita el más antiguo, para que el archivo no crezca sin límite. Prográmalo a diario con cron o con un temporizador de systemd en el host:

15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1

También puedes dejar que la pila lo programe: el perfil backup ejecuta backup-scheduler, que crea un respaldo base cada QUIRE_BACKUP_INTERVAL_HOURS (24 de forma predeterminada), y backup-offsite, que se describe a continuación.

docker compose -f docker/compose.yaml --profile backup up -d

Copias cifradas fuera del host

Los dos volúmenes están en el mismo host que la base de datos, y un respaldo guardado en la máquina que falló no sirve. backup-offsite copia cada respaldo base y cada segmento WAL archivado a un almacén independiente a través del puerto de almacenamiento, los cifra y los conserva según el periodo establecido:

  • Cifrado. AES-256-GCM con QUIRE_BACKUP_ENCRYPTION_KEY (o el archivo indicado por QUIRE_BACKUP_ENCRYPTION_KEY_FILE): 32 bytes que se generan con openssl rand -hex 32. Cada archivo tiene un nonce y una etiqueta de autenticación propios, así que nadie puede leer una copia sin la clave y se detecta cualquier modificación. Guarda la clave junto con QUIRE_MASTER_KEY, lejos de este host y del almacén de respaldos. Sin la clave no se puede restaurar.
  • Ubicación. QUIRE_BACKUP_STORAGE_DRIVER puede ser s3, azure o local (un disco remoto montado en QUIRE_BACKUP_STORAGE_ROOT). La configuración es la del almacenamiento de archivos con el prefijo QUIRE_BACKUP_: QUIRE_BACKUP_S3_ENDPOINT, QUIRE_BACKUP_S3_BUCKET, QUIRE_BACKUP_S3_ACCESS_KEY_ID, entre otras. Usa un bucket distinto y, de ser posible, una cuenta diferente de la de archivos; si el proveedor lo permite, usa credenciales que puedan escribir pero no borrar.
  • Retención. Conserva los respaldos base más recientes según QUIRE_BACKUP_OFFSITE_KEEP (de forma predeterminada usa QUIRE_BACKUP_KEEP o, si no se configuró, 7) y el WAL necesario para el más antiguo; elimina del almacén los conjuntos y segmentos anteriores.
  • Frecuencia. Cada QUIRE_BACKUP_SHIP_INTERVAL_SECONDS (300 de forma predeterminada). El envío es idempotente: se omite lo que ya está en el almacén y un respaldo base solo cuenta como almacenado después de escribir su manifiesto, al final.

También puedes ejecutar manualmente el mismo comando:

docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts ship
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts verify

Para restaurar en un host nuevo, recupera primero un conjunto. Luego sigue los pasos de abajo y usa el directorio descargado en lugar del volumen pgbackup, y el archivo wal-archive recuperado en lugar de pgwal:

bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore

Restaurar a un momento dado

Hazlo si se pierden datos: por una importación defectuosa, porque se eliminó un curso o porque necesitas deshacer una migración de contracción. Esto reemplaza la base de datos activa, así que primero ensaya el proceso con el simulacro de abajo.

  1. Elige la hora de destino, en UTC y justo antes del daño: 2026-09-24 09:30:00+00. El registro de auditoría (/admin/audit) suele indicar el momento.
  2. Detén todos los procesos que escriben: docker compose -f docker/compose.yaml stop web content worker scheduler collab.
  3. Conserva el clúster dañado hasta verificar la restauración:
    docker compose -f docker/compose.yaml stop postgres
    docker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
  4. Descomprime en el volumen de datos el respaldo base más reciente anterior al destino y solicita una recuperación dirigida:
    docker run --rm -v quire_pgbackup:/backups:ro -v quire_postgres18-data:/var/lib/postgresql postgres:18-alpine sh -euc '
      base="$(ls -1d /backups/base-* | sort | tail -n 1)"   # or the one before the target
      rm -rf /var/lib/postgresql/18/docker && mkdir -p /var/lib/postgresql/18/docker
      tar -xzf "$base/base.tar.gz" -C /var/lib/postgresql/18/docker
      touch /var/lib/postgresql/18/docker/recovery.signal
      chown -R postgres:postgres /var/lib/postgresql/18/docker && chmod 700 /var/lib/postgresql/18/docker'
  5. Recupera: inicia Postgres una vez con la configuración de recuperación, mediante un archivo de reemplazo de Compose para no modificar el archivo habitual:
    # docker/compose.recover.yaml
    services:
      postgres:
        command: [postgres, -c, "restore_command=cp /var/lib/postgresql/wal-archive/%f %p",
                  -c, "recovery_target_time=2026-09-24 09:30:00+00",
                  -c, recovery_target_action=promote, -c, archive_mode=off,
                  -c, max_connections=200, -c, hba_file=/etc/postgresql/pg_hba.conf]
    El archivo de reemplazo sustituye el comando completo, por lo que repite las dos configuraciones que necesita la recuperación: max_connections no puede ser inferior al valor de la base principal (de lo contrario, la recuperación se detiene con el error “insufficient parameter settings”) y hay que indicar pg_hba.conf montado.
    docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgres
    docker compose -f docker/compose.yaml logs -f postgres   # wait for "database system is ready"
  6. Verifica el resultado antes de permitir el acceso: comprueba la cadena de auditoría (docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts) y que los datos perdidos hayan regresado.
  7. Vuelve al funcionamiento normal: ejecuta docker compose -f docker/compose.yaml up -d. Postgres se reinicia con el archivado activado y comienza una nueva línea temporal de WAL. Enseguida crea un respaldo base nuevo.

El nombre de proyecto quire se antepone a cada volumen; docker volume ls muestra los nombres exactos.

Simulacro de verificación programado

backup-offsite también ejecuta un simulacro cada QUIRE_BACKUP_DRILL_INTERVAL_HOURS (168 de forma predeterminada, cada semana) y vuelve a intentarlo en la siguiente ejecución si falla. Recupera el respaldo base fuera del host más reciente y todos los segmentos WAL posteriores, los descifra (así comprueba que la clave aún los abre y que no se modificaron), compara cada archivo con su manifiesto, confirma que el archivo sea un directorio de datos de Postgres y verifica que no haya huecos en el WAL a partir del respaldo. El informe se guarda en el almacén como reports/drill-<time>.json y en los registros del servicio; si el simulacro falla, indica el archivo o el primer segmento faltante.

Simulacro de restauración

Un respaldo que nunca se restauró no sirve como respaldo. El simulacro restaura el respaldo base completo más reciente anterior al destino y el archivo WAL en una instancia temporal de Postgres que no comparte nada con la instancia activa; luego comprueba el resultado:

docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"

La hora de destino debe estar en UTC y con ese formato exacto. Se necesita un respaldo base anterior a ella y WAL archivado posterior. En una instalación nueva, crea un respaldo base y espera al siguiente segmento archivado (como máximo un minuto si hay escrituras) antes de elegir una hora posterior al respaldo. El simulacro requiere Docker y bash en el host, y nada más.

Cualquiera de estos pasos puede hacer que falle:

  1. Posibilidad de recuperación: el clúster temporal reproduce el WAL hasta la hora de destino y se abre.
  2. Integridad de los datos: compara el número de filas de cada tabla con la base activa (tooling/restore-drill). La base activa contiene cambios posteriores al destino, así que una tabla puede diferir en cualquier dirección hasta el valor mayor entre 500 filas y una décima parte de su tamaño (las escrituras reducen la cantidad restaurada; las eliminaciones hacen que la restauración tenga más). Una partición creada después del destino no cuenta como tabla perdida. En una instalación con más actividad, amplía el margen con QUIRE_DRILL_MAX_BEHIND y QUIRE_DRILL_MAX_DRIFT_RATIO. Si falta una tabla o está vacía, la verificación falla.
  3. Integridad: comprueba la cadena hash de auditoría en la copia restaurada.
  4. Usabilidad: el rol de la aplicación puede leer con seguridad a nivel de fila.
  5. Tiempo: compara el tiempo total hasta la confirmación con QUIRE_DRILL_RTO_SECONDS (3600 de forma predeterminada).

Nunca escribe en la base ni en los volúmenes activos: los volúmenes de respaldos y WAL se montan en modo de solo lectura, y al terminar se elimina el clúster temporal, tanto si pasa como si falla.

Configura QUIRE_DRILL_REPORT con una ruta para guardar un informe JSON, ya sea que pase o falle, y programa la ejecución desde el host Docker:

30 3 1 * * cd /srv/quire && QUIRE_DRILL_REPORT=/var/log/quire-drill.json docker/scripts/restore-drill.sh >> /var/log/quire-drill.log 2>&1

Ejecútalo cada mes y antes de cada actualización. Un simulacro fallido bloquea la actualización. Una vez por trimestre, pide a alguien que no haya redactado este manual que haga una restauración real a un momento dado en un host de prueba, usando solo este documento.

Archivos

Los archivos locales están en el volumen files. Respalda ese volumen junto con la base de datos, al mismo tiempo, y restaura ambos juntos:

docker run --rm -v quire_files:/files:ro -v "$PWD":/out alpine tar -czf /out/files-$(date -u +%Y%m%d).tar.gz -C /files .

Si usas almacenamiento de objetos, activa el control de versiones del bucket y conserva durante 35 días las versiones que dejaron de ser actuales. Así, la recuperación de archivos a un momento dado queda a cargo del propio bucket.

Navegación

Escribe para buscar…

↑↓ navegar↵ seleccionarEsc cerrar