Vai al contenuto

Backup, ripristino point-in-time e prova di ripristino

Esegui backup di Quire, ripristinalo a un momento preciso e dimostralo con la prova di ripristino.

Il progetto è nella sezione 8 di docs/architecture/23-ops.md. Questo è il manuale operativo per il prodotto Docker Compose. È scritto per essere seguito da chi non l’ha scritto; se un passaggio non è chiaro, è un difetto di questo documento.

Cosa è protetto, e come

Risorsa Come Dove
Il database WAL archiviato in continuo, al massimo ogni 60 secondi, dal primo avvio volume pgwal
Il database Backup di base con pg_basebackup, giornalieri per impostazione predefinita (backup-scheduler) volume pgbackup
Il database Copie cifrate dei backup di base e del WAL, ogni cinque minuti (backup-offsite) Un archivio separato da te nominato
File Il volume files. Copialo con lo strumento di backup del tuo host, oppure usa object storage versionato volume files
Segreti docker/.env, soprattutto QUIRE_MASTER_KEY (e ogni QUIRE_MASTER_KEY_RETIRED ancora in uso), QUIRE_BACKUP_ENCRYPTION_KEY, e docker/secrets/audit-signing-key.pem Conservane una copia fuori da questo host
Indici di ricerca, cache, resi Non sottoposti a backup; ricostruiti

Obiettivi: un punto di recupero entro 60 secondi dal guasto, e un ripristino entro 60 minuti per un database da 500 GB.

Due errori sono comuni. Un database ripristinato senza i suoi file mostra pagine rotte. Un database ripristinato senza QUIRE_MASTER_KEY non può decifrare le credenziali SSO, webhook e di integrazione che contiene; finché una rotazione della chiave master non termina senza irrisolti (key-rotation.md), ciò include le chiavi ritirate. Entrambi fanno parte del backup.

Fare backup

Un backup di base dell’intero cluster:

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

Conserva i QUIRE_BACKUP_KEEP backup di base più recenti (predefiniti 5) ed elimina il WAL che il più vecchio non serve più, così l’archivio non può crescere senza limiti. Programmalo giornalmente con cron o un timer systemd sull’host:

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

Oppure lascia che lo stack lo programmi: il profilo backup esegue backup-scheduler, che fa un backup di base ogni QUIRE_BACKUP_INTERVAL_HOURS (predefinito 24), e backup-offsite, descritto di seguito.

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

Copie cifrate fuori host

Entrambi i volumi vivono sullo stesso host del database, e un backup sulla macchina guasta non è un backup. backup-offsite copia ogni backup di base e ogni segmento WAL archiviato in un archivio separato tramite la porta storage, cifrati, e li conserva lì secondo retention:

  • Cifratura. AES-256-GCM con QUIRE_BACKUP_ENCRYPTION_KEY (oppure il file nominato da QUIRE_BACKUP_ENCRYPTION_KEY_FILE): 32 byte, da openssl rand -hex 32. Ogni file ha il proprio nonce e un tag di autenticazione, così una copia è illeggibile senza la chiave e qualsiasi sua modifica viene rilevata. Conserva la chiave con QUIRE_MASTER_KEY, lontano da questo host e lontano dall’archivio di backup. Senza la chiave non c’è ripristino.
  • Dove. QUIRE_BACKUP_STORAGE_DRIVER è s3, azure oppure local (un disco remoto montato in QUIRE_BACKUP_STORAGE_ROOT). Le impostazioni sono quelle del file storage con prefisso QUIRE_BACKUP_: QUIRE_BACKUP_S3_ENDPOINT, QUIRE_BACKUP_S3_BUCKET, QUIRE_BACKUP_S3_ACCESS_KEY_ID, e così via. Usa un bucket diverso e, idealmente, un account diverso dai file, con credenziali che possano scrivere ma non eliminare se il provider lo consente.
  • Retention. I QUIRE_BACKUP_OFFSITE_KEEP backup di base più recenti (predefinito QUIRE_BACKUP_KEEP, altrimenti 7) e il WAL di cui il più vecchio ha bisogno; i set e segmenti più vecchi vengono eliminati dall’archivio.
  • Quando. Ogni QUIRE_BACKUP_SHIP_INTERVAL_SECONDS (predefinito 300). L’invio è idempotente: ciò che è già archiviato viene saltato, e un backup di base conta come archiviato solo una volta scritto il suo manifest, per ultimo.

Lo stesso comando si esegue a mano:

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

Per ripristinare su un nuovo host, riporta prima indietro un set, poi segui i passaggi sotto con la directory recuperata al posto del volume pgbackup e il wal-archive recuperato al posto di pgwal:

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

Ripristino a un momento preciso

Usalo dopo una perdita di dati: un’importazione errata, un corso eliminato, una migrazione di consolidamento da annullare. Sostituisce il database live, quindi provalo prima con la prova sotto.

  1. Scegli il momento target, in UTC, appena prima del danno: 2026-09-24 09:30:00+00. Il registro di audit (/admin/audit) di solito mostra il momento.
  2. Ferma tutto ciò che scrive: docker compose -f docker/compose.yaml stop web content worker scheduler collab
  3. Conserva il cluster danneggiato finché il ripristino non è verificato:
    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. Estrai il backup di base più recente prima del target nel volume dati, e chiedi un recupero mirato:
    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: avvia Postgres una volta con le impostazioni di recupero, da un override Compose così il file normale resta intatto:
    # 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]
    L’override sostituisce l’intero comando, quindi ripete le due impostazioni da cui il recupero dipende: max_connections non inferiore a quello del primario (altrimenti il recupero si interrompe con “insufficient parameter settings”) e il pg_hba.conf montato.
    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. Verificalo prima di far entrare chiunque: la catena di audit (docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts), e che i dati persi siano tornati.
  7. Torna alla normalità: docker compose -f docker/compose.yaml up -d. Questo riavvia Postgres con archiviazione attiva, e inizia una nuova timeline WAL. Fai subito un nuovo backup di base.

Il nome progetto quire prefissa ciascun volume; docker volume ls mostra i nomi esatti.

La prova di verifica programmata

backup-offsite esegue anche una prova ogni QUIRE_BACKUP_DRILL_INTERVAL_HOURS (predefinito 168, settimanale), e di nuovo al passaggio successivo dopo un fallimento. Recupera il backup di base fuori host più recente e ogni segmento WAL successivo, decifra ciascuno (il che prova che la chiave li apre ancora e nulla è stato alterato), confronta ciascun file col suo manifest, verifica che l’archivio sia una directory dati Postgres, e verifica che il WAL dal backup in poi non abbia buchi. Il report è scritto nello store come reports/drill-<time>.json e nel registro del servizio; una prova fallita nomina il file o il primo segmento mancante.

La prova di ripristino

Un backup mai ripristinato non è un backup. La prova ripristina il backup di base completo più recente prima del target, più l’archivio WAL, in un Postgres provvisorio che non condivide nulla col live, e dimostra il risultato:

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

Il target è UTC esattamente in quella forma. Servono un backup di base precedente a esso e WAL archiviato oltre esso: su una nuova installazione, fai un backup di base e attendi il prossimo segmento archiviato (al massimo un minuto con scritture) prima di scegliere un target dopo il backup. La prova richiede Docker e bash sull’host, nient’altro.

Ogni passaggio boccia la prova:

  1. Recuperabilità: il cluster provvisorio rigioca fino al target e si apre.
  2. Completezza: conteggio righe di ogni tabella contro il database live (tooling/restore-drill). Il database live è andato avanti dal target, quindi una tabella può differire per il maggiore tra 500 righe e un decimo della sua dimensione, in entrambe le direzioni (le scritture lo fanno restare indietro, le eliminazioni fanno sì che il ripristino contenga di più); una partizione creata dopo il target non è una tabella persa. Allarga la tolleranza su un’installazione più trafficata con QUIRE_DRILL_MAX_BEHIND e QUIRE_DRILL_MAX_DRIFT_RATIO. Una tabella mancante o svuotata boccia.
  3. Integrità: la catena hash di audit si verifica sulla copia ripristinata.
  4. Usabilità: il ruolo applicativo legge tramite la sicurezza a livello di riga.
  5. Tempo: dall’avvio al verde, contro QUIRE_DRILL_RTO_SECONDS (predefinito 3600).

Non scrive mai sul database live né sui suoi volumi: i volumi di backup e WAL sono montati in sola lettura e il cluster provvisorio viene rimosso alla fine, che la prova passi o fallisca.

Imposta QUIRE_DRILL_REPORT a un percorso per far scrivere un report JSON, che la prova passi o fallisca, ed eseguila su programma dall’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

Eseguila mensilmente e prima di ogni aggiornamento. Una prova fallita blocca l’aggiornamento. Una volta al trimestre, chiedi a chi non ha scritto questo manuale di eseguire un vero ripristino point-in-time su un host di riserva, usando solo questo documento.

File

I file locali vivono nel volume files. Sottoponili a backup col database, nello stesso momento, e ripristina entrambi insieme:

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 .

Con object storage, attiva il versionamento del bucket e conserva 35 giorni di versioni non correnti; il ripristino point-in-time per i file è allora quello proprio del bucket.

Navigazione

Digita per cercare…

↑↓ per spostarti↵ per selezionareEsc per chiudere