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
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), también se necesitan las claves retiradas. Ambos forman parte de la copia de seguridad.
Crear copias de seguridad
Para crear una copia base de todo el clúster:
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:
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.
docker compose -f docker/compose.yaml --profile backup up -d
Copias cifradas fuera del host
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:
docker compose -f docker/compose.yaml run --rm backup-offsite bun apps/worker/src/backups/main.ts shipdocker 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:
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
Restaurar a un momento dado
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.
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ó.
Detén todos los procesos que escriben: docker compose -f docker/compose.yaml stop web content worker scheduler collab.
Conserva el clúster dañado hasta verificar la restauración:
docker compose -f docker/compose.yaml stop postgresdocker run --rm -v quire_postgres18-data:/from -v quire_postgres-damaged:/to alpine cp -a /from/. /to/
Descomprime en el volumen de datos la copia base más reciente anterior al momento de destino y habilita la 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'
Recupera: inicia Postgres una vez con los ajustes de recuperación, mediante una configuración alternativa de Compose para no modificar el archivo normal:
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.
docker compose -f docker/compose.yaml -f docker/compose.recover.yaml up -d postgresdocker compose -f docker/compose.yaml logs -f postgres # wait for "database system is ready"
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.
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
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
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:
docker/scripts/restore-drill.sh # to ninety minutes agodocker/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:
Posibilidad de recuperación: el clúster temporal reproduce los registros hasta el destino y se abre.
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.
Integridad criptográfica: se verifica la cadena hash de auditoría en la copia restaurada.
Usabilidad: el rol de la aplicación puede leer mediante la seguridad a nivel de fila.
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:
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
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:
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.