Aller au contenu

Bases de données

PostgreSQL et MySQL suivent la même recette en quatre temps :

  1. fetch — récupérer le dump ;
  2. unpack — le décompresser si besoin ;
  3. start_sandbox — démarrer un serveur neuf, vide, jetable ;
  4. restore_postgres / restore_mysql — rejouer le dump dedans.

Puis les sondes comptent ce qui est arrivé dans ce serveur.

La version du bac à sable

Prenez la même version majeure qu'en production. Un dump pris sur un serveur plus récent ne se rejoue pas sur un serveur plus ancien. Si votre plan échoue sur une erreur de syntaxe SQL incompréhensible, c'est presque toujours ça.

Les mots de passe ci-dessous (demo-docmost, restore) sont ceux du conteneur jetable créé pour l'exécution. Ils n'ont aucun rapport avec votre serveur d'origine. Voir aussi comment adapter le bloc fetch et comment trouver les seuils.

PostgreSQL

La sauvegarde en entrée

Un pg_dump compressé, un fichier par nuit, dans un dossier que le runner peut lire : docmost_2026-09-02T03-00.sql.gz.

Le champ format de l'étape restore_postgres dit comment le rejouer :

Votre dump format Outil utilisé
pg_dump sans option (SQL en clair) plain psql
pg_dump -Fc (format custom) custom pg_restore
pg_dump -Fd (répertoire) directory pg_restore
vous ne savez pas auto détecté au contenu

Le plan

{
  "steps": [
    {
      "type": "fetch",
      "name": "Récupérer le dump Docmost",
      "config": {
        "source": "local",
        "path": "/backups/docmost/postgres",
        "file_pattern": "docmost_*.sql.gz",
        "selection_strategy": "latest_modified",
        "max_age": "3h",
        "output": "docmost.sql.gz"
      }
    },
    {
      "type": "unpack",
      "name": "Décompresser",
      "config": { "format": "gzip", "input": "docmost.sql.gz", "output": "docmost.sql" }
    },
    {
      "type": "start_sandbox",
      "name": "Démarrer PostgreSQL",
      "config": {
        "image": "postgres:16-alpine",
        "name": "demo-docmost-restore",
        "env": { "POSTGRES_DB": "docmost", "POSTGRES_USER": "docmost", "POSTGRES_PASSWORD": "demo-docmost" },
        "healthcheck": { "cmd": ["pg_isready", "-U", "docmost"], "interval": "5s", "timeout": "5s", "retries": 12 }
      }
    },
    {
      "type": "restore_postgres",
      "name": "Restaurer la base",
      "config": {
        "database": "docmost",
        "user": "docmost",
        "password": "demo-docmost",
        "input": "docmost.sql",
        "format": "plain"
      }
    }
  ],
  "probes": [
    {
      "name": "schema",
      "type": "postgres",
      "timeout": 60,
      "config": {
        "PGHOST": "demo-docmost-restore",
        "PGDATABASE": "docmost",
        "PGUSER": "docmost",
        "PGPASSWORD": "demo-docmost",
        "RESTOREPROOF_PG_TABLE": "information_schema.tables WHERE table_schema = 'public'",
        "RESTOREPROOF_PG_ASSERT": "gte",
        "RESTOREPROOF_PG_EXPECTED_ROWS": "10"
      }
    },
    {
      "name": "comptes",
      "type": "postgres",
      "timeout": 60,
      "config": {
        "PGHOST": "demo-docmost-restore",
        "PGDATABASE": "docmost",
        "PGUSER": "docmost",
        "PGPASSWORD": "demo-docmost",
        "RESTOREPROOF_PG_TABLE": "users",
        "RESTOREPROOF_PG_ASSERT": "gte",
        "RESTOREPROOF_PG_EXPECTED_ROWS": "1"
      }
    }
  ]
}

Ce que chaque sonde prouve

  • schema — le dump a recréé au moins dix tables dans le schéma public. C'est le contrôle grossier : il attrape le dump tronqué qui s'arrête au milieu, pas la table vidée.
  • comptes — il reste au moins un compte. Une base parfaitement restaurée mais sans utilisateur est une base dans laquelle personne n'entre.

Le nom de la sonde (schema, comptes) est libre : c'est ce que vous lirez dans le rapport, écrivez-le pour vous.

Les variables de la sonde postgres

Dans l'éditeur, vous remplissez un champ cible au format mabase ou mabase/matable ; l'éditeur en dérive PGDATABASE et RESTOREPROOF_PG_TABLE. En JSON, écrivez directement :

  • connexion : PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE (les variables RESTOREPROOF_PG_HOST, RESTOREPROOF_PG_PORT, RESTOREPROOF_PG_USER, RESTOREPROOF_PG_PASSWORD, RESTOREPROOF_PG_DATABASE, RESTOREPROOF_PG_SSLMODE marchent aussi) ;
  • ce qu'on compte : RESTOREPROOF_PG_TABLE, qui accepte une clause WHERE et même une jointure, ou RESTOREPROOF_PG_QUERY pour votre propre SELECT ;
  • l'affirmation : RESTOREPROOF_PG_ASSERT (gte, eq, lte, zero) et RESTOREPROOF_PG_EXPECTED_ROWS.

zero est l'opérateur qui prouve une absence : « aucune ligne ne correspond ». C'est lui qui attrape les données incohérentes — un article sans auteur, une entrée sans propriétaire. Voir Gitea et Vaultwarden pour des plans qui s'en servent.

Trouver les bons nombres

10 et 1 ci-dessus sont les valeurs de la pile de démonstration. Pour votre base : laissez 1 partout, lancez un essai, et laissez l'écran vous proposer les seuils à partir de ce que la sonde a réellement compté. Les chiffres remontent dans le rapport, vous n'avez rien à compter vous-même.

Un cas à connaître : quand la cible ne nomme qu'une base et pas une table, le nombre observé est un nombre de tables, pas un nombre de lignes. RestoreProof ne vous propose alors pas ce chiffre comme seuil de lignes — il voudrait dire tout autre chose une fois figé.

MySQL

La sauvegarde en entrée

Un mysqldump compressé : wordpress_2026-09-02T03-00.sql.gz. Pas de champ format ici : restore_mysql rejoue le fichier avec le client mysql.

Le plan

C'est le plan WordPress de la pile de démonstration ; hors WordPress, seuls les noms de tables changent.

{
  "steps": [
    {
      "type": "fetch",
      "name": "Récupérer le dump WordPress",
      "config": {
        "source": "local",
        "path": "/backups/wordpress/mysql",
        "file_pattern": "wordpress_*.sql.gz",
        "selection_strategy": "latest_modified",
        "max_age": "3h",
        "output": "wordpress.sql.gz"
      }
    },
    {
      "type": "unpack",
      "name": "Décompresser",
      "config": { "format": "gzip", "input": "wordpress.sql.gz", "output": "wordpress.sql" }
    },
    {
      "type": "start_sandbox",
      "name": "Démarrer MySQL",
      "config": {
        "image": "mysql:8.0",
        "name": "demo-wp-restore",
        "env": { "MYSQL_ROOT_PASSWORD": "restore", "MYSQL_DATABASE": "wordpress" },
        "healthcheck": {
          "cmd": ["mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-prestore"],
          "interval": "5s",
          "timeout": "5s",
          "retries": 24
        }
      }
    },
    {
      "type": "restore_mysql",
      "name": "Restaurer la base",
      "config": {
        "database": "wordpress",
        "user": "root",
        "password": "restore",
        "input": "wordpress.sql"
      }
    }
  ],
  "probes": [
    {
      "name": "wp_admin",
      "type": "mysql",
      "timeout": 60,
      "config": {
        "MYSQL_HOST": "demo-wp-restore",
        "MYSQL_DATABASE": "wordpress",
        "MYSQL_USER": "root",
        "MYSQL_PASSWORD": "restore",
        "RESTOREPROOF_MYSQL_TABLE": "wp_users",
        "RESTOREPROOF_MYSQL_ASSERT": "gte",
        "RESTOREPROOF_MYSQL_EXPECTED_ROWS": "1"
      }
    },
    {
      "name": "wp_content",
      "type": "mysql",
      "timeout": 60,
      "config": {
        "MYSQL_HOST": "demo-wp-restore",
        "MYSQL_DATABASE": "wordpress",
        "MYSQL_USER": "root",
        "MYSQL_PASSWORD": "restore",
        "RESTOREPROOF_MYSQL_TABLE": "wp_posts WHERE post_status = 'publish' AND post_type = 'post'",
        "RESTOREPROOF_MYSQL_ASSERT": "gte",
        "RESTOREPROOF_MYSQL_EXPECTED_ROWS": "12"
      }
    },
    {
      "name": "wp_site_url",
      "type": "mysql",
      "timeout": 60,
      "config": {
        "MYSQL_HOST": "demo-wp-restore",
        "MYSQL_DATABASE": "wordpress",
        "MYSQL_USER": "root",
        "MYSQL_PASSWORD": "restore",
        "RESTOREPROOF_MYSQL_TABLE": "wp_options WHERE option_name IN ('siteurl','home') AND option_value <> ''",
        "RESTOREPROOF_MYSQL_ASSERT": "eq",
        "RESTOREPROOF_MYSQL_EXPECTED_ROWS": "2"
      }
    },
    {
      "name": "wp_orphans",
      "type": "mysql",
      "timeout": 60,
      "config": {
        "MYSQL_HOST": "demo-wp-restore",
        "MYSQL_DATABASE": "wordpress",
        "MYSQL_USER": "root",
        "MYSQL_PASSWORD": "restore",
        "RESTOREPROOF_MYSQL_TABLE": "wp_posts p LEFT JOIN wp_users u ON u.ID = p.post_author WHERE p.post_author <> 0 AND u.ID IS NULL",
        "RESTOREPROOF_MYSQL_ASSERT": "zero"
      }
    }
  ]
}

Ce que chaque sonde prouve

  • wp_admin — au moins un compte peut encore se connecter.
  • wp_content — les articles publiés sont revenus. C'est le nombre qui distingue un site restauré d'une installation neuve : une installation neuve a déjà toutes ses tables wp_, et zéro contenu à vous.
  • wp_site_urlsiteurl et home sont là et non vides. Sans elles, WordPress sert un site que personne n'atteint, aussi complète que soit la restauration. eq: 2 parce qu'on en attend exactement deux.
  • wp_orphans — aucun article n'a perdu son auteur. zero : la restauration n'a pas seulement rempli les tables, elle a gardé les liens entre elles.

Les variables de la sonde mysql

Mêmes règles que PostgreSQL : MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE (ou leurs équivalents RESTOREPROOF_MYSQL_HOST, RESTOREPROOF_MYSQL_PORT, RESTOREPROOF_MYSQL_USER, RESTOREPROOF_MYSQL_PASSWORD, RESTOREPROOF_MYSQL_DATABASE, plus RESTOREPROOF_MYSQL_CHARSET et RESTOREPROOF_MYSQL_TLS), puis RESTOREPROOF_MYSQL_TABLE ou RESTOREPROOF_MYSQL_QUERY, RESTOREPROOF_MYSQL_ASSERT et RESTOREPROOF_MYSQL_EXPECTED_ROWS.

MariaDB se teste avec le même plan : changez l'image du bac à sable pour mariadb:11 ou mariadb:10.11.

Le préfixe des tables

wp_ est un défaut, pas une règle. Un site qui l'a changé a besoin de son propre préfixe partout, sinon les quatre sondes interrogent des tables qui n'existent pas — et une sonde qui échoue parce qu'elle regarde au mauvais endroit ne prouve rien sur votre sauvegarde. Le modèle WordPress Database de l'interface demande ce préfixe comme première question.