---
title: "Copias de seguridad, recuperación a un momento dado y simulacro de restauración"
description: "Haz una copia de seguridad de Quire, restáurala a un momento dado y compruébala con el simulacro de restauración."
image: "https://docs.quirelms.com/og.png"
---

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

# Copias de seguridad, 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 se describe en la sección 8 de `docs/architecture/23-ops.md`. Este es el manual de operaciones del producto Docker Compose. Está redactado para que pueda seguirlo alguien que no lo haya escrito; si algún paso no queda claro, este documento tiene un defecto.

## 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, con intervalos máximos de 60 segundos desde el primer inicio | Volumen `pgwal` |
| La base de datos | Copias base con `pg_basebackup`, a diario de forma predeterminada (`backup-scheduler`) | Volumen `pgbackup` |
| La base de datos | Copias cifradas de las copias base y del WAL, cada cinco minutos (`backup-offsite`) | Almacén independiente que indiques |
| Archivos | Volumen `files`. Cópialo con la herramienta de copia de seguridad del host o utiliza almacenamiento de objetos con versiones | Volumen `files` |
| Secretos | `docker/.env`, sobre todo `QUIRE_MASTER_KEY` (y cualquier `QUIRE_MASTER_KEY_RETIRED` que siga en uso), `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 incluyen en la copia de seguridad; se vuelven a generar | |

Objetivos: un punto de recuperación situado 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 frecuentes. Si se restaura una base de datos **sin sus archivos**, las páginas aparecerán rotas. Si se restaura una base de datos **sin `QUIRE_MASTER_KEY`**, no podrán descifrarse las credenciales de SSO, webhooks e integraciones que contiene; hasta que termine una rotación de clave maestra sin valores pendientes de resolver ([key-rotation.md](/es/ops/key-rotation/)), también se necesitan las claves retiradas. Ambos forman parte de la copia de seguridad.

## Crear copias de seguridad <!--quire:taking-backups-->

Para crear una copia base de todo el clúster:

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

Conserva las `QUIRE_BACKUP_KEEP` copias base más recientes (5 de forma predeterminada) y elimina el WAL que ya no necesite la copia más antigua, para que el archivo no crezca sin límite. Programa una ejecución diaria 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 permitir que la pila la programe: el perfil `backup` ejecuta `backup-scheduler`, que crea una copia 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-->

Ambos volúmenes están en el mismo host que la base de datos, y una copia en el equipo que falló no es una copia de seguridad. `backup-offsite` copia al almacenamiento independiente todas las copias base y los segmentos WAL archivados, a través del puerto de almacenamiento, cifrados y sujetos a la política de retención:

- **Cifrado.** AES-256-GCM con `QUIRE_BACKUP_ENCRYPTION_KEY` (o el archivo indicado por `QUIRE_BACKUP_ENCRYPTION_KEY_FILE`): 32 bytes, generados con `openssl rand -hex 32`. Cada archivo tiene un nonce y una etiqueta de autenticación propios, de modo que no se 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 copias. 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 misma que la del almacenamiento de archivos, con el prefijo `QUIRE_BACKUP_`: `QUIRE_BACKUP_S3_ENDPOINT`, `QUIRE_BACKUP_S3_BUCKET`, `QUIRE_BACKUP_S3_ACCESS_KEY_ID`, etc. Utiliza un bucket distinto del de archivos y, si es posible, otra cuenta; configura credenciales que puedan escribir pero no eliminar, si el proveedor lo permite.
- **Retención.** Conserva las copias base más recientes, según `QUIRE_BACKUP_OFFSITE_KEEP` (de forma predeterminada `QUIRE_BACKUP_KEEP` o, si no se establece, 7) y el WAL que necesite la copia base más antigua; elimina del almacén las copias y los segmentos anteriores.
- **Frecuencia.** Cada `QUIRE_BACKUP_SHIP_INTERVAL_SECONDS` (300 de forma predeterminada). El envío es idempotente: omite lo que ya se almacenó, y una copia base solo cuenta como almacenada cuando se escribe 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 y sigue estos pasos utilizando el directorio descargado en lugar del volumen `pgbackup` y el `wal-archive` descargado 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 después de perder datos: por una importación defectuosa, al borrar un curso o tras una migración de contracción que tengas que deshacer. Sustituye la base de datos activa, así que primero ensaya el proceso con el simulacro que aparece abajo.

1. **Elige el momento 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 cuándo ocurrió.
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** la copia base más reciente anterior al momento de destino y habilita la 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 los ajustes de recuperación, mediante una configuración alternativa de Compose para no modificar el archivo normal:
   ```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]
   ```
   La configuración alternativa sustituye el comando completo, por lo que repite los dos ajustes que necesita la recuperación: `max_connections` no puede ser inferior al valor de la instancia principal (de lo contrario, la recuperación se detiene con «insufficient parameter settings») y se debe 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. **Comprueba la restauración** antes de permitir el acceso: verifica la cadena de auditoría (`docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts`) y confirma que los datos perdidos han vuelto.
7. **Vuelve al funcionamiento normal** con `docker compose -f docker/compose.yaml up -d`. Así se reinicia Postgres con el archivado activado y empieza una nueva línea temporal de WAL. Crea enseguida una nueva copia base.

El nombre del proyecto `quire` se antepone al nombre de 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, es decir, una vez a la semana), y vuelve a intentarlo en la siguiente ejecución después de un fallo. Recupera la copia base externa más reciente y cada segmento WAL posterior, los descifra (lo que demuestra que la clave todavía permite abrirlos y que no se han alterado), compara cada archivo con su manifiesto, comprueba que el archivo es un directorio de datos de Postgres y verifica que no haya huecos en el WAL desde la copia base. El informe se guarda en el almacén como `reports/drill-<time>.json` y en el registro del servicio; un simulacro fallido indica el archivo o el primer segmento que falta.

## Simulacro de restauración <!--quire:the-restore-drill-->

Una copia de seguridad que nunca se ha restaurado no es una copia de seguridad. El simulacro restaura la copia base completa más reciente anterior al momento de destino, junto con el archivo WAL, en un Postgres temporal que no comparte nada con la instancia activa y 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"
```

El destino es una fecha UTC con ese formato exacto. Necesita una copia base anterior al destino y WAL archivado posterior; en una instalación nueva, crea una copia base y espera al siguiente segmento archivado (como máximo un minuto si hay escrituras) antes de elegir una fecha posterior a la copia. El simulacro solo necesita Docker y bash en el host.

Cada paso puede hacer que el simulacro falle:

1. **Posibilidad de recuperación**: el clúster temporal reproduce los registros hasta el destino y se abre.
2. **Integridad**: se comparan los recuentos de filas de cada tabla con la base de datos activa (`tooling/restore-drill`). La base activa ha seguido cambiando desde el momento de destino, así que puede haber una diferencia en cualquier dirección equivalente al mayor de estos valores: 500 filas o una décima parte del tamaño de la tabla (las escrituras reducen el recuento restaurado; las eliminaciones hacen que la restauración tenga más filas). Una partición creada después del destino no cuenta como una tabla perdida. Para instalaciones con mucha 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 comprobación falla.
3. **Integridad criptográfica**: se verifica la cadena hash de auditoría en la copia restaurada.
4. **Usabilidad**: el rol de la aplicación puede leer mediante la seguridad a nivel de fila.
5. **Duración**: el tiempo desde el inicio hasta la aprobación se compara con `QUIRE_DRILL_RTO_SECONDS` (3600 de forma predeterminada).

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

Establece `QUIRE_DRILL_REPORT` con una ruta para guardar un informe JSON, tanto si el simulacro pasa como si falla, y ejecútalo según un calendario en el host de 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 mensualmente y antes de cada actualización. Un simulacro fallido bloquea la actualización. Una vez por trimestre, pide a alguien que no haya escrito este manual que realice una restauración real a un momento dado en un host de reserva, usando solo este documento.

## Archivos <!--quire:files-->

Los archivos locales están en el volumen `files`. Inclúyelo en la copia de seguridad 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 35 días las versiones que ya no sean actuales; así, la recuperación de archivos a un momento dado depende del propio bucket.

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