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 shipdocker 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.
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.
Ferma tutto ciò che scrive:
docker compose -f docker/compose.yaml stop web content worker scheduler collab
Conserva il cluster danneggiato finché il ripristino non è verificato:
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/
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'
Recupera: avvia Postgres una volta con le impostazioni di recupero, da un
override Compose così il file normale resta intatto:
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 postgresdocker compose -f docker/compose.yaml logs -f postgres # wait for "database system is ready"
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.
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 agodocker/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:
Recuperabilità: il cluster provvisorio rigioca fino al target e si apre.
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.
Integrità: la catena hash di audit si verifica sulla copia ripristinata.
Usabilità: il ruolo applicativo legge tramite la sicurezza a livello di riga.
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:
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.