Guide pratique

Sauvegarder MySQL et MariaDB vers S3 : le guide complet, de mysqldump à la restauration vérifiée

Ce que mysqldump laisse dehors, les options qui comptent, un script qui ne peut pas rendre 0 sur un fichier vide, la restauration à la main dans un conteneur jetable, et les quatre requêtes qui prouvent qu'une base MySQL restaurée contient vos données.

Septembre 2026· 11 min de lecture·en

Sauvegarder MySQL vers S3 : de mysqldump à la restauration vérifiée

La plupart des guides de sauvegarde MySQL s'arrêtent à la ligne de commande mysqldump. C'est la partie facile. Ce qui décide si vous récupérez vos données, c'est tout ce qui l'entoure : ce que le dump laisse dehors, les options que vous avez passées, le fait que le script échoue vraiment quand le dump échoue, et le fait que quelqu'un ait déjà rejoué le fichier au moins une fois.

Ce guide monte la chaîne entière. À la fin, vous saurez ce qu'une sauvegarde MySQL doit contenir en plus des tables, quelles options de mysqldump changent vraiment quelque chose, comment écrire un script qui ne peut pas annoncer un succès sur un fichier vide, comment restaurer le dump à la main dans un conteneur jetable, et quelles quatre requêtes SQL disent qu'une base restaurée contient réellement vos données. MariaDB suit le même chemin, à l'image du conteneur près.

Tout cela fonctionne avec mysqldump, docker et l'AWS CLI, et rien d'autre. La dernière section montre comment faire tourner cette même restauration et ces mêmes vérifications sur une planification plutôt qu'à la main — c'est le travail de RestoreProof — mais la procédure tient debout seule, et le jour où ça compte, c'est elle que vous suivrez.

Ce qu'une sauvegarde MySQL doit contenir

mysqldump écrit du SQL : les CREATE TABLE et les INSERT des bases qu'on lui nomme. Ce qui vit au niveau du serveur n'y est pas :

  • les comptes et leurs droits, qui sont dans la base mysql ;
  • les autres bases du même serveur, si vous n'avez pas passé --databases ;
  • la configuration du serveur (my.cnf, /etc/mysql/conf.d/).

Deux choses s'oublient plus discrètement encore, parce que mysqldump ne les prend pas par défaut : les procédures et fonctions stockées et les événements du planificateur. Les déclencheurs, eux, sont pris par défaut. Une base restaurée sans ses procédures se lit parfaitement et ne calcule plus rien.

Le moteur de stockage est écrit dans le dump, sur chaque CREATE TABLE, sous la forme ENGINE=InnoDB. Il doit donc exister sur le serveur où vous restaurez : un dump de tables MyISAM se rejoue sur un MySQL 8, mais un dump produit avec un moteur tiers s'arrête sur la première table. Comptez vos moteurs une fois sur la production, avec select engine, count(*) from information_schema.tables group by engine, et vous saurez à quoi vous avez affaire.

Les options qui comptent

OptionCe qu'elle change
--single-transactionun instantané cohérent des tables InnoDB, sans verrouiller la production
--routines --eventsajoute les procédures, les fonctions et les événements, absents sans elle
--triggersdéjà actif par défaut : ne le désactivez pas
--databasesmet un CREATE DATABASE et un USE dans le dump, qui recrée donc la base lui-même
--set-gtid-purged=OFFretire le SET @@GLOBAL.gtid_purged que porte un dump pris sur un serveur en GTID

--single-transaction ne protège que ce qui est transactionnel. Sur des tables MyISAM, il ne fait rien du tout : la seule cohérence possible passe par --lock-tables, qui bloque les écritures pendant le dump. Une base qui mélange les deux moteurs ne peut pas être sauvegardée de façon cohérente en une seule commande — c'est une bonne raison de finir de migrer vers InnoDB.

--set-gtid-purged=OFF ne sert que si votre serveur tourne avec GTID activé. Dans ce cas le dump commence par fixer la position de réplication, ce qu'un serveur d'essai qui a déjà exécuté des transactions refuse de rejouer.

Le script de sauvegarde

#!/bin/bash
set -euo pipefail

ts=$(date +%Y%m%d_%H%M%S)
dest=s3://sauvegardes-facturation/mysql
cnf=/etc/mysql/sauvegarde.cnf

mysqldump --defaults-extra-file="${cnf}" \
  --single-transaction --routines --events --triggers \
  --databases facturation \
  | gzip > "/backups/facturation_${ts}.sql.gz"

mysql --defaults-extra-file="${cnf}" -N -B \
  -e "select user, host from mysql.user where user not like 'mysql.%'" \
  | while read -r u h; do
      mysql --defaults-extra-file="${cnf}" -N -B \
        -e "show grants for '${u}'@'${h}'"
    done | sed 's/$/;/' > "/backups/droits_${ts}.sql"

aws s3 cp "/backups/facturation_${ts}.sql.gz" "${dest}/"
aws s3 cp "/backups/droits_${ts}.sql" "${dest}/"

Le mot de passe est dans sauvegarde.cnf, sous une section [client], et pas sur la ligne de commande : un -pmotdepasse est lisible par n'importe qui dans la liste des processus pendant toute la durée du dump.

La boucle show grants produit un fichier de droits rejouable. SHOW GRANTS rend ses lignes sans point-virgule final, d'où le sed : sans lui, le fichier se rejoue comme une seule instruction et échoue.

Sans pipefail, un dump raté ressort en succès

Dans mysqldump | gzip, le shell ne regarde que le code de sortie du dernier maillon. gzip réussit à compresser une sortie vide, donc le script rend 0 et le cron est content. set -o pipefail — inclus dans le set -euo pipefail ci-dessus — fait échouer la ligne entière dès que mysqldump échoue. C'est la première cause de sauvegardes vides qui passent au vert pendant des mois.

Gardez l'horodatage dans le nom du fichier : un fichier toujours écrasé au même nom ne laisse aucune chance de revenir à la veille. Pour la rétention, une règle de cycle de vie sur le bucket fait le travail sans script à maintenir.

Reste à le lancer chaque nuit. Un cron suffit, à deux conditions : que sa sortie d'erreur aille quelque part où quelqu'un la lit, et qu'une exécution qui s'éternise n'en croise pas une autre — flock -n sur un fichier verrou règle le second point en une ligne.

Restaurer une fois, à la main

Une sauvegarde n'est prouvée que restaurée. Faites-le une fois, entièrement, sur un poste jetable — c'est aussi la procédure que vous suivrez le jour où ça compte.

docker run -d --name mysql-essai -e MYSQL_ROOT_PASSWORD=essai mysql:8.4
until docker exec mysql-essai mysqladmin ping -uroot -pessai --silent; do
  sleep 1
done

gunzip -c /backups/facturation_20260918_010000.sql.gz \
  | docker exec -i mysql-essai mysql -uroot -pessai

Aucune base n'est nommée à la restauration : le dump a été pris avec --databases, il porte donc son propre CREATE DATABASE facturation.

Le client mysql s'arrête à la première erreur, et c'est ce qu'on veut. N'ajoutez pas --force dans un essai de restauration : il transforme un dump tronqué en base à moitié remplie qui a l'air de marcher.

Deux détails qui font perdre une soirée. Si votre client vient de MariaDB — c'est le cas dans les images Alpine — il vérifie le certificat du serveur et refuse de se connecter à un MySQL 8 qui présente son certificat auto-signé : --skip-ssl sur mysql, mysqldump et mysqladmin lève le blocage pour un essai local. Et le conteneur répond au ping avant d'avoir fini son initialisation, d'où l'attente ci-dessus plutôt qu'un sleep fixe.

Prenez le temps que ça a pris. C'est votre durée de restauration réelle, la seule à comparer au délai que vous avez promis.

Ce qu'on vérifie dans une base restaurée

Le dump s'est rejoué sans erreur ne veut pas dire que les données sont là. Quatre questions, dans cet ordre :

select count(*) from information_schema.tables where table_schema = 'facturation';
select count(*) from factures;
select count(*) from factures where created_at > now() - interval 2 day;
select auto_increment from information_schema.tables
  where table_schema = 'facturation' and table_name = 'factures';
  1. Les tables sont là. Zéro table, c'est un dump vide qui s'est rejoué parfaitement.
  2. Les lignes sont là. Comparez à l'ordre de grandeur de la production, pas à un chiffre exact : une base qui grossit, c'est normal.
  3. Les données sont récentes. La question se pose en comptant les lignes d'hier, pas en lisant une date : un nombre se compare à un seuil, une date non. Zéro ici, c'est un job de sauvegarde qui a cessé de tourner.
  4. Les compteurs ont suivi. Un AUTO_INCREMENT retombé à 1 provoque des collisions de clés dès la première écriture après la restauration.

Puis détruisez tout : docker rm -f mysql-essai.

Automatiser cette vérification

Ce qui précède coûte une heure ou deux, à chaque fois. C'est pour cette raison que ces tests, faits à la main, finissent par ne plus être faits.

RestoreProof rejoue exactement ces étapes en tâche planifiée, sur votre infrastructure : un runner récupère la sauvegarde, la restaure dans un conteneur jetable, pose les mêmes questions que ci-dessus, détruit tout, et signe le résultat. Les données ne sortent pas de chez vous.

Déclarez d'abord la sauvegarde comme source — le bucket, le préfixe, le motif *.sql.gz, et la stratégie le plus récemment modifié. Les clés d'accès ne se saisissent pas : le plan porte une référence, env://AWS_ACCESS_KEY_ID, que le runner résout dans son propre environnement. Voir les références de secrets et comment adapter le bloc fetch.

L'assistant MySQL Database écrit ensuite le plan, et ce plan est la manipulation que vous venez de faire à la main, ligne pour ligne :

À la mainDans le plan
aws s3 cp depuis le bucketfetch, qui prend le fichier le plus récent
gunzip -cunpack
docker run mysql:8.4start_sandbox
mysql sans --forcerestore_mysql
les quatre requêtesune sonde mysql par question
docker rm -fle nettoyage, toujours exécuté

Le seul ajout est max_age: 26h, et c'est celui qu'une restauration ne peut pas déduire du contenu : il fait échouer l'exécution quand la sauvegarde la plus récente trouvée à la source est plus vieille que ce délai. Le plan complet, prêt à coller dans l'éditeur, est dans la recette bases de données.

Les deux premières questions se posent en nommant une table, les deux autres en écrivant la requête. Une sonde qui porte sa propre requête doit rendre un nombre : c'est à ce nombre que le seuil s'applique.

La version du bac à sable

mysql:8.4 doit reprendre la version majeure de votre serveur. Un dump pris sur un serveur plus récent ne se rejoue pas sur un serveur plus ancien. Pour MariaDB, le plan est le même : seule l'image change, en mariadb:11 ou mariadb:10.11.

Les seuils ne se recopient pas depuis cette page. Un essai restaure votre sauvegarde, compte ce qu'elle contient réellement, et propose chaque seuil 5 % sous la valeur mesurée, avec l'opérateur gte. Voir comment trouver les seuils.

L'écran de fin d'essai : chaque sonde MySQL avec la valeur mesurée et le seuil proposé cinq pour cent en dessous

Il reste à choisir une fréquence. Chaque nuit place l'exécution à 2 h du matin : votre script de sauvegarde tourne à 1 h, le dump a donc une heure quand il est éprouvé. L'autre déclenchement possible est un appel HTTP à la fin de ce script — le test porte alors exactement sur le fichier qui vient d'être produit.

Chaque exécution laisse un rapport horodaté, signé, qui nomme la sauvegarde éprouvée et ce que chaque sonde a mesuré.

Ce que cette chaîne ne prouve pas

Elle prouve qu'une sauvegarde récente se restaure dans un MySQL neuf et qu'elle contient les données attendues. Elle ne prouve rien sur les comptes et leurs droits, qui vivent dans un autre fichier : pour les éprouver, il faut rejouer le fichier de droits et se connecter avec un de ces comptes. Elle ne prouve pas non plus que votre application fonctionne sur cette base : pour cela, il faut la démarrer contre le bac à sable et ajouter une sonde http. Et elle ne dit rien de ce qui a été écrit depuis le dernier dump — cet écart, c'est votre RPO, et il se règle avec la fréquence des sauvegardes, pas avec les tests.

Éprouver ses sauvegardes sans y penser

RestoreProof rejoue ces étapes sur votre infrastructure, aussi souvent que vous le décidez : il récupère la sauvegarde, la restaure dans un conteneur jetable, pose les mêmes questions, détruit tout, et signe le résultat. Vos données ne sortent pas de votre réseau.

FAQ

Qu'est-ce que mysqldump ne sauvegarde pas ?

Les comptes et leurs droits, qui vivent dans la base mysql, les autres bases du serveur si vous n'avez pas passé --databases, et la configuration du serveur. Les procédures stockées, les fonctions et les événements ne sont pas pris par défaut : il faut --routines --events. Les déclencheurs, eux, sont pris par défaut.

--single-transaction suffit-il pour une sauvegarde cohérente ?

Sur des tables InnoDB, oui, et sans bloquer les écritures. Sur des tables MyISAM, il ne fait rien : ces tables ne sont pas transactionnelles, et la seule cohérence possible passe par --lock-tables, qui bloque la production pendant le dump.

Pourquoi un dump refuse-t-il de se rejouer avec une erreur sur gtid_purged ?

Parce qu'il a été pris sur un serveur où GTID est activé : le fichier commence alors par positionner SET @@GLOBAL.gtid_purged, et un serveur d'essai qui a déjà exécuté des transactions refuse cette ligne. --set-gtid-purged=OFF au moment du dump la retire, et le fichier se rejoue partout.

Comment tester une sauvegarde MariaDB ?

Avec la même procédure et le même plan : seule l'image du conteneur jetable change, en mariadb:11 ou mariadb:10.11. Le format du dump est le même SQL, et le client MariaDB le rejoue.

Comment sauvegarder les comptes MySQL et leurs droits ?

Pas avec le dump de votre base : ils vivent dans la base mysql. On produit un fichier rejouable en parcourant les comptes de mysql.user et en demandant SHOW GRANTS pour chacun. Une précaution : SHOW GRANTS rend ses lignes sans point-virgule final, et sans lui le fichier se rejoue comme une seule instruction et échoue.

sauvegarde mysqlmysqldump s3test de restauration mysqlrestaurer un dump mysqlsauvegarde mariadbvérifier une sauvegarde mysql
Disponible

Prêt à prouver vos restaurations ?

RestoreProof automatise les tests de restauration et génère des preuves signées cryptographiquement — sans que vos données quittent votre infrastructure.