La conception est décrite à la section 8 de docs/architecture/23-ops.md. Voici le guide d’exploitation du produit Docker Compose. Il est rédigé pour être suivi par une personne qui ne l’a pas écrit; toute étape qui manque de clarté constitue un défaut de ce document.
Éléments protégés et méthode
Élément
Méthode
Emplacement
Base de données
WAL archivé en continu, au plus toutes les 60 secondes, dès le premier démarrage
Volume pgwal
Base de données
Sauvegardes de base avec pg_basebackup, quotidiennes par défaut (backup-scheduler)
Volume pgbackup
Base de données
Copies chiffrées des sauvegardes de base et du WAL toutes les cinq minutes (backup-offsite)
Stockage distinct que vous désignez
Fichiers
Volume files. Copiez-le avec l’outil de sauvegarde de votre hôte ou utilisez un stockage d’objets versionné
Volume files
Secrets
docker/.env, surtout QUIRE_MASTER_KEY (et tout QUIRE_MASTER_KEY_RETIRED encore utilisé), QUIRE_BACKUP_ENCRYPTION_KEY et docker/secrets/audit-signing-key.pem
Gardez une copie à l’extérieur de cet hôte
Index de recherche, caches, rendus
Non sauvegardés; ils sont reconstruits
Objectifs : un point de récupération datant d’au plus 60 secondes avant la panne et une restauration en moins de 60 minutes pour une base de données de 500 Go.
Deux erreurs sont fréquentes. Une base restaurée sans ses fichiers produit des pages brisées. Une base restaurée sans QUIRE_MASTER_KEY ne peut pas déchiffrer les justificatifs SSO, de webhooks et d’intégrations qu’elle contient; tant qu’une rotation de clé principale n’est pas terminée sans élément non résolu (rotation des clés), les clés retirées en font aussi partie. Ces deux éléments sont inclus dans la sauvegarde.
Effectuer les sauvegardes
Sauvegarde de base du groupe complet :
docker compose -f docker/compose.yaml --profile backup run --rm backup
Elle conserve les sauvegardes de base les plus récentes, jusqu’au nombre défini par QUIRE_BACKUP_KEEP (5 par défaut), et supprime le WAL dont la plus ancienne sauvegarde n’a plus besoin; l’archive ne peut donc pas croître sans limite. Planifiez-la tous les jours au moyen de cron ou d’un minuteur systemd sur l’hôte :
15 2 * * * cd /srv/quire && docker compose -f docker/compose.yaml --profile backup run --rm backup >> /var/log/quire-backup.log 2>&1
Vous pouvez aussi laisser la pile la planifier : le profil backup exécute backup-scheduler, qui réalise une sauvegarde de base toutes les QUIRE_BACKUP_INTERVAL_HOURS heures (24 par défaut), ainsi que backup-offsite, décrit ci-dessous.
docker compose -f docker/compose.yaml --profile backup up -d
Copies chiffrées hors hôte
Les deux volumes se trouvent sur le même hôte que la base de données; une sauvegarde conservée sur la machine en panne n’est pas une sauvegarde. backup-offsite copie chaque sauvegarde de base et chaque segment WAL archivé dans un stockage distinct, par le port de stockage, les chiffre et les conserve selon les règles de rétention :
Chiffrement. AES-256-GCM avec QUIRE_BACKUP_ENCRYPTION_KEY (ou le fichier indiqué par QUIRE_BACKUP_ENCRYPTION_KEY_FILE) : 32 octets, générés par openssl rand -hex 32. Chaque fichier a son propre nonce et sa balise d’authentification; une copie est donc illisible sans la clé et toute modification est détectée. Gardez cette clé avec QUIRE_MASTER_KEY, à l’extérieur de cet hôte et du stockage des sauvegardes. Sans elle, aucune restauration n’est possible.
Emplacement.QUIRE_BACKUP_STORAGE_DRIVER prend la valeur s3, azure ou local (disque distant monté à QUIRE_BACKUP_STORAGE_ROOT). Les paramètres sont ceux du stockage de fichiers précédés de QUIRE_BACKUP_ : QUIRE_BACKUP_S3_ENDPOINT, QUIRE_BACKUP_S3_BUCKET, QUIRE_BACKUP_S3_ACCESS_KEY_ID, etc. Utilisez un compartiment distinct de celui des fichiers et, idéalement, un compte distinct; les justificatifs devraient permettre l’écriture sans permettre la suppression si le fournisseur le permet.
Rétention. Les sauvegardes de base les plus récentes, jusqu’au nombre QUIRE_BACKUP_OFFSITE_KEEP (valeur par défaut : QUIRE_BACKUP_KEEP, sinon 7), ainsi que le WAL nécessaire à la plus ancienne sont conservés; les jeux et segments plus anciens sont supprimés du stockage.
Fréquence. Toutes les QUIRE_BACKUP_SHIP_INTERVAL_SECONDS secondes (300 par défaut). Le transfert est idempotent : les éléments déjà stockés sont ignorés, et une sauvegarde de base est considérée comme stockée uniquement après l’écriture de son manifeste, en dernier.
La même commande peut être exécutée manuellement :
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
Pour restaurer sur un nouvel hôte, rapatriez d’abord un jeu de sauvegarde, puis suivez les étapes ci-dessous en utilisant le répertoire récupéré au lieu du volume pgbackup, et le wal-archive récupéré au lieu de pgwal :
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
Restaurer à un instant précis
À utiliser après une perte de données, comme une mauvaise importation, la suppression d’un cours ou une migration de retrait à annuler. Cette opération remplace la base de données active; répétez-la d’abord avec l’exercice ci-dessous.
Choisissez l’heure cible, en UTC, juste avant le dommage : 2026-09-24 09:30:00+00. Le journal d’audit (/admin/audit) indique généralement le moment.
Arrêtez tout processus qui écrit : docker compose -f docker/compose.yaml stop web content worker scheduler collab.
Conservez le groupe endommagé jusqu’à la vérification de la restauration :
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/
Décompressez dans le volume de données la sauvegarde de base la plus récente antérieure à l’heure cible, puis demandez une récupération ciblée :
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'
Effectuez la récupération : démarrez PostgreSQL une fois avec les paramètres de récupération, dans un fichier de remplacement Compose afin de ne pas modifier le fichier normal :
Le remplacement change toute la commande; il répète donc les deux paramètres requis pour la récupération : max_connections doit être au moins aussi élevé que celui de la base principale (sinon la récupération s’interrompt avec « insufficient parameter settings ») et le fichier pg_hba.conf monté.
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"
Vérifiez le résultat avant de laisser quiconque se connecter : vérifiez la chaîne d’audit avec docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts et confirmez le rétablissement des données perdues.
Reprenez le fonctionnement normal : docker compose -f docker/compose.yaml up -d. PostgreSQL redémarre avec l’archivage activé et une nouvelle chronologie WAL commence. Effectuez tout de suite une nouvelle sauvegarde de base.
Le nom de projet quire précède celui de chaque volume; docker volume ls affiche les noms exacts.
Exercice de vérification planifié
backup-offsite exécute aussi un exercice toutes les QUIRE_BACKUP_DRILL_INTERVAL_HOURS heures (168, soit une fois par semaine, par défaut), puis de nouveau au passage suivant après un échec. Il récupère la sauvegarde de base hors hôte la plus récente et chaque segment WAL qui la suit, les déchiffre (ce qui confirme que la clé les ouvre toujours et qu’ils n’ont pas été modifiés), compare chaque fichier à son manifeste, vérifie que l’archive est un répertoire de données PostgreSQL et confirme que le WAL à partir de la sauvegarde ne comporte aucun intervalle manquant. Le rapport est enregistré dans le stockage sous reports/drill-<time>.json et dans le journal du service; un exercice échoué indique le fichier concerné ou le premier segment manquant.
Exercice de restauration
Une sauvegarde qui n’a jamais été restaurée n’en est pas une. L’exercice restaure, dans un PostgreSQL temporaire entièrement distinct de la base active, la sauvegarde de base complète la plus récente antérieure à l’heure cible, ainsi que l’archive WAL, puis en vérifie le résultat :
docker/scripts/restore-drill.sh # to ninety minutes agodocker/scripts/restore-drill.sh --target "2026-09-24 09:30:00+00"
L’heure cible doit être en UTC et respecter exactement cette forme. Il faut une sauvegarde de base antérieure et un WAL archivé postérieur : sur une nouvelle installation, faites une sauvegarde de base et attendez le prochain segment archivé (une minute au plus s’il y a des écritures) avant de choisir une heure cible postérieure à la sauvegarde. L’exercice exige seulement Docker et bash sur l’hôte.
Chaque étape est nécessaire à la réussite de l’exercice :
Récupérabilité : le groupe temporaire rejoue jusqu’à l’heure cible et s’ouvre.
Intégralité : comparez le nombre de lignes de chaque table avec la base active (tooling/restore-drill). La base active a évolué depuis l’heure cible; le nombre d’une table peut donc différer, dans un sens ou dans l’autre, de la plus grande valeur entre 500 lignes et un dixième de sa taille (les écritures le font prendre du retard, les suppressions font que la restauration en contient davantage). Une partition créée après l’heure cible n’est pas une table perdue. Augmentez la marge sur une installation plus occupée avec QUIRE_DRILL_MAX_BEHIND et QUIRE_DRILL_MAX_DRIFT_RATIO. Une table manquante ou vide entraîne un échec.
Intégrité : vérifiez la chaîne de hachage d’audit dans la copie restaurée.
Utilisabilité : le rôle d’application peut lire en respectant la sécurité au niveau des lignes.
Durée : le temps entre le démarrage et la réussite respecte QUIRE_DRILL_RTO_SECONDS (3600 par défaut).
L’exercice n’écrit jamais dans la base active ni dans ses volumes : les volumes de sauvegarde et de WAL sont montés en lecture seule, et le groupe temporaire est supprimé à la fin, que l’exercice réussisse ou échoue.
Définissez QUIRE_DRILL_REPORT sur un chemin pour produire un rapport JSON, en cas de réussite ou d’échec, puis planifiez l’exercice depuis l’hôte Docker :
Exécutez-le chaque mois et avant chaque mise à niveau. Un exercice échoué bloque la mise à niveau. Une fois par trimestre, demandez à une personne qui n’a pas rédigé ce guide de procéder à une véritable restauration à un instant précis sur un hôte de secours, en suivant uniquement ce document.
Fichiers
Les fichiers locaux se trouvent dans le volume files. Sauvegardez-le en même temps que la base de données et restaurez les deux ensemble :
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 .
Avec un stockage d’objets, activez le versionnement du compartiment et conservez 35 jours de versions non courantes; le stockage assure alors la récupération des fichiers à un instant précis.