---
title: "Backup, ripristino point-in-time e prova di ripristino"
description: "Esegui backup di Quire, ripristinalo a un momento preciso e dimostralo con la prova di ripristino."
image: "https://docs.quirelms.com/og.png"
---

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

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

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

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

| 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](/it/ops/key-rotation/)),
ciò include le chiavi ritirate. Entrambi fanno parte del backup.

## Fare backup <!--quire:taking-backups-->

Un backup di base dell'intero cluster:

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

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

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

## Copie cifrate fuori host <!--quire:encrypted-off-host-copies-->

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:

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

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

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

## Ripristino a un momento preciso <!--quire:restoring-to-a-point-in-time-->

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:
   ```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. **Estrai il backup di base più recente prima del target** nel volume dati,
   e chiedi un recupero mirato:
   ```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**: avvia Postgres una volta con le impostazioni di recupero, da un
   override Compose così il file normale resta intatto:
   ```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]
   ```
   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.
   ```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. **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 <!--quire:the-scheduled-verification-drill-->

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

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:

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

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

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

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

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

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.

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