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 conçu pour être suivi par une personne qui ne l’a pas rédigé ; toute étape peu claire constitue un défaut de ce document.
Éléments protégés et méthode
Élément
Méthode
Emplacement
Base de données
Archivage continu du WAL, 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)
Magasin distinct que vous désignez
Fichiers
Volume files. Copiez-le avec l’outil de sauvegarde de l’hôte ou utilisez un stockage d’objets avec gestion des versions
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
Conservez une copie hors de cet hôte
Index de recherche, caches, dérivés
Non sauvegardés ; reconstruits
Objectifs : point de récupération datant de moins de 60 secondes avant la panne, et restauration d’une base de données de 500 Go en 60 minutes.
Deux erreurs sont fréquentes. Une base restaurée sans ses fichiers produit des pages cassées. Une base restaurée sans QUIRE_MASTER_KEY ne peut pas déchiffrer les identifiants SSO, webhook et d’intégration qu’elle contient ; tant que la rotation de la clé principale n’est pas terminée sans valeur non résolue (key-rotation.md), cela concerne également les clés retirées. Les deux font partie de la sauvegarde.
Effectuer des sauvegardes
Pour sauvegarder la totalité du cluster :
docker compose -f docker/compose.yaml --profile backup run --rm backup
La commande conserve les QUIRE_BACKUP_KEEP sauvegardes de base les plus récentes (5 par défaut) et supprime le WAL dont la plus ancienne n’a plus besoin, afin que l’archive ne grossisse pas indéfiniment. Planifiez son exécution quotidienne avec cron ou 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 planifier l’opération : le profil backup démarre backup-scheduler, qui réalise une sauvegarde de base toutes les QUIRE_BACKUP_INTERVAL_HOURS (24 par défaut), ainsi que backup-offsite, décrit ci-après.
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 stockée sur la machine qui est tombée en panne n’en est pas une. backup-offsite copie chaque sauvegarde de base et chaque segment WAL archivé vers un magasin distinct par le port de stockage, les chiffre et les conserve conformément à la durée de rétention :
Chiffrement. AES-256-GCM avec QUIRE_BACKUP_ENCRYPTION_KEY (ou le fichier désigné par QUIRE_BACKUP_ENCRYPTION_KEY_FILE) : 32 octets générés par openssl rand -hex 32. Chaque fichier possède son propre nonce et code d’authentification ; sans la clé, la copie est illisible et toute modification est détectée. Conservez cette clé avec QUIRE_MASTER_KEY, hors de cet hôte et du magasin de sauvegarde. Sans clé, aucune restauration n’est possible.
Emplacement.QUIRE_BACKUP_STORAGE_DRIVER vaut s3, azure ou local (disque distant monté dans QUIRE_BACKUP_STORAGE_ROOT). Les paramètres de stockage des fichiers sont repris avec le préfixe QUIRE_BACKUP_ : QUIRE_BACKUP_S3_ENDPOINT, QUIRE_BACKUP_S3_BUCKET, QUIRE_BACKUP_S3_ACCESS_KEY_ID, etc. Utilisez un bucket différent de celui des fichiers et, idéalement, un compte distinct ; dans la mesure du possible, ses identifiants doivent autoriser l’écriture mais pas la suppression.
Rétention. Conservez les QUIRE_BACKUP_OFFSITE_KEEP sauvegardes de base les plus récentes (QUIRE_BACKUP_KEEP par défaut, sinon 7) et le WAL nécessaire à la plus ancienne ; les ensembles et segments précédents sont supprimés du magasin.
Fréquence. Toutes les QUIRE_BACKUP_SHIP_INTERVAL_SECONDS secondes (300 par défaut). L’envoi est idempotent : les éléments déjà stockés sont ignorés et une sauvegarde de base n’est considérée comme enregistrée qu’après l’écriture finale de son manifeste.
Vous pouvez exécuter la même commande 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 ensemble, puis suivez les étapes ci-dessous en remplaçant le volume pgbackup par le répertoire récupéré et en utilisant le wal-archive récupéré à la place de pgwal :
bun apps/worker/src/backups/main.ts fetch base-20260924T021500Z /srv/restore
Restaurer à un instant donné
Utilisez cette procédure après une perte de données : importation incorrecte, cours supprimé ou migration de retrait à annuler. Elle remplace la base en service ; répétez donc d’abord le test ci-dessous.
Choisissez l’heure cible, en UTC, juste avant l’incident :
2026-09-24 09:30:00+00. Le journal d’audit (/admin/audit) indique généralement le moment.
Arrêtez tous les services qui écrivent :
docker compose -f docker/compose.yaml stop web content worker scheduler collab
Conservez le cluster 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 précédant 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'
Récupérez les données : démarrez Postgres une fois avec les paramètres de récupération, au moyen d’une surcharge Compose afin de ne pas modifier le fichier habituel :
La surcharge remplace la commande entière ; elle répète donc les deux paramètres nécessaires à la récupération : max_connections ne doit pas être inférieur à celui du serveur principal (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 d’autoriser les connexions : contrôlez la chaîne d’audit (docker compose -f docker/compose.yaml run --rm worker bun tooling/audit-verify/run.ts) et vérifiez que les données perdues sont revenues.
Revenez à la normale : docker compose -f docker/compose.yaml up -d. Postgres redémarre avec l’archivage activé et une nouvelle chronologie WAL commence. Effectuez immédiatement une nouvelle sauvegarde de base.
Le nom de projet quire préfixe chaque volume ; docker volume ls affiche leurs noms exacts.
Test de vérification planifié
backup-offsite effectue aussi un test toutes les QUIRE_BACKUP_DRILL_INTERVAL_HOURS (168 par défaut, soit une fois par semaine), et le relance au passage suivant après tout échec. Il récupère la sauvegarde de base hors hôte la plus récente et tous les segments WAL postérieurs, déchiffre chacun d’eux (pour vérifier 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 Postgres et que le WAL ne comporte aucune lacune depuis la sauvegarde. Le rapport est enregistré dans le magasin sous reports/drill-<time>.json et dans le journal du service ; un test échoué indique le fichier ou le premier segment manquant.
Test de restauration
Une sauvegarde qui n’a jamais été restaurée n’en est pas vraiment une. Le test restaure la sauvegarde de base complète la plus récente antérieure à l’heure cible ainsi que l’archive WAL dans une instance Postgres temporaire isolée de celle en service, puis 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 est au format UTC indiqué. Il faut une sauvegarde de base antérieure et des journaux WAL archivés au-delà de cette heure : sur une nouvelle installation, effectuez d’abord une sauvegarde de base et attendez le segment archivé suivant (une minute au plus s’il y a des écritures) avant de choisir une cible postérieure à la sauvegarde. Le test nécessite Docker et bash sur l’hôte, rien d’autre.
Le test échoue à chacune des étapes suivantes :
Récupérabilité : le cluster temporaire rejoue le WAL jusqu’à l’heure cible et démarre.
Complétude : le nombre de lignes de chaque table est comparé à celui de la base active (tooling/restore-drill). La base active a évolué depuis l’heure cible ; une table peut donc différer de la plus grande valeur entre 500 lignes et un dixième de sa taille, dans un sens ou dans l’autre (les écritures augmentent le retard, les suppressions font que la restauration en contient davantage). Une partition créée après la cible n’est pas une table perdue. Sur une installation plus active, élargissez la tolérance avec QUIRE_DRILL_MAX_BEHIND et QUIRE_DRILL_MAX_DRIFT_RATIO. Une table manquante ou vide provoque un échec.
Intégrité : la chaîne de hachage de l’audit est vérifiée sur la copie restaurée.
Utilisabilité : le rôle applicatif peut lire les données grâce à la sécurité au niveau des lignes.
Durée : mesurez le temps écoulé entre le lancement et le résultat positif, par rapport à QUIRE_DRILL_RTO_SECONDS (3600 par défaut).
Le test n’écrit jamais dans la base en service ni dans ses volumes : les volumes de sauvegarde et de WAL sont montés en lecture seule, et le cluster temporaire est supprimé à la fin, en cas de réussite comme d’échec.
Définissez QUIRE_DRILL_REPORT avec un chemin pour produire un rapport JSON, en cas de réussite comme d’échec, puis planifiez le test sur l’hôte Docker :
Exécutez-le chaque mois et avant chaque mise à niveau. Un test échoué bloque la mise à niveau. Une fois par trimestre, demandez à quelqu’un qui n’a pas rédigé ce guide d’effectuer une véritable restauration à un instant donné sur un hôte de réserve, 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 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 bucket et conservez 35 jours de versions non actuelles ; la récupération des fichiers à un instant donné relève alors du bucket lui-même.