---
title: "Sauvegarde, récupération à un instant donné et test de restauration"
description: "Sauvegardez Quire, restaurez-le à un instant donné et vérifiez l’opération avec le test de restauration."
image: "https://docs.quirelms.com/og.png"
---

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

# Sauvegarde, récupération à un instant donné et test de restauration

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

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

| É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](/fr/ops/key-rotation/)), cela concerne également les clés retirées. Les deux font partie de la sauvegarde.

## Effectuer des sauvegardes <!--quire:taking-backups-->

Pour sauvegarder la totalité du cluster :

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

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

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

## Copies chiffrées hors hôte <!--quire:encrypted-off-host-copies-->

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 :

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

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

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

## Restaurer à un instant donné <!--quire:restoring-to-a-point-in-time-->

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.

1. **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.
2. **Arrêtez tous les services qui écrivent** :
   `docker compose -f docker/compose.yaml stop web content worker scheduler collab`
3. **Conservez le cluster endommagé** jusqu’à la vérification de la restauration :
   ```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. **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 :
   ```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. **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 :
   ```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]
   ```
   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é.
   ```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. **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.
7. **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é <!--quire:the-scheduled-verification-drill-->

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

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 :

```sh
docker/scripts/restore-drill.sh                                  # to ninety minutes ago
docker/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 :

1. **Récupérabilité** : le cluster temporaire rejoue le WAL jusqu’à l’heure cible et démarre.
2. **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.
3. **Intégrité** : la chaîne de hachage de l’audit est vérifiée sur la copie restaurée.
4. **Utilisabilité** : le rôle applicatif peut lire les données grâce à la sécurité au niveau des lignes.
5. **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 :

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

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

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 :

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

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.

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