---
title: "Respaldos, recuperación a un momento dado y simulacro de restauración"
description: "Respalda Quire, restáuralo a un momento dado y comprueba el proceso con un simulacro de restauración."
image: "https://docs.quirelms.com/og.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.quirelms.com/es-419/llms.txt
> Use this file to discover all available pages before exploring further.

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

<span id="backup-point-in-time-recovery-and-the-restore-drill"></span>

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 <!--quire:what-is-protected-and-how-->

| 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](/es-419/ops/key-rotation/)), también se necesitan las claves retiradas. Todo eso forma parte del respaldo.

## Crear respaldos <!--quire:taking-backups-->

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

```sh
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:

```cron
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.

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

## Copias cifradas fuera del host <!--quire:encrypted-off-host-copies-->

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:

```sh
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`:

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

## Restaurar a un momento dado <!--quire:restoring-to-a-point-in-time-->

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:
   ```sh
   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:
   ```sh
   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:
   ```yaml
   # 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.
   ```sh
   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 <!--quire:the-scheduled-verification-drill-->

`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 <!--quire:the-restore-drill-->

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:

```sh
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:

```cron
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 <!--quire:files-->

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:

```sh
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.

Source: https://docs.quirelms.com/es-419/ops/backup-restore/index.mdx
