Skip to content

Sauvegardes ​

Une instance Mankomail garde son état à deux endroits, PostgreSQL et le stockage des blobs, et dépend d’un secret, ENCRYPTION_KEY. Une sauvegarde utile couvre les trois. Le code source livre deux scripts, scripts/backup.sh et scripts/restore.sh, qui fonctionnent avec le fichier Docker Compose de référence et avec chaque stockage que l’application accepte : MinIO, tout service compatible S3 (OVH Object Storage, AWS S3, Garage…), ou un répertoire local (STORAGE_DRIVER=fs).

Ce que doit contenir une sauvegarde ​

Où cela vitDans la sauvegarde de backup.sh
L’état : membres, boîtes, workflows, exécutions, file de tâches, tables, sessions, secrets chiffrésPostgreSQLOui : database.dump
Corps de mails, pièces jointes, fichiers ajoutés aux exécutionsStockage des blobs : un bucket S3, ou un répertoire avec STORAGE_DRIVER=fsOui : blobs/
ENCRYPTION_KEYVotre fichier .envNon, jamais

Restaurer les deux premiers sans le troisième donne une instance qui démarre et affiche tous les mails, mais dont aucun secret enregistré n’est lisible : chaque boîte doit être reconnectée, et chaque clé de fournisseur d’IA et chaque connexion saisies de nouveau.

Gardez, hors du serveur et à part des sauvegardes :

  • ENCRYPTION_KEY, dans un gestionnaire de mots de passe ou un coffre ;
  • le reste du fichier .env : mots de passe de la base et du stockage, PUBLIC_BASE_URL ;
  • l’identifiant et le secret de vos applications OAuth Google et Microsoft. Le secret est dans la base, mais chiffré avec la clé de l’instance.

Ce dont les scripts ont besoin ​

  • Lancez-les sur l’hôte Docker, depuis un clone du code source, avec Docker et le plugin docker compose v2.
  • Ils lisent le fichier .env et le fichier Compose à la racine du dépôt. Utilisez ENV_FILE et COMPOSE_FILE pour désigner d’autres chemins.
  • Ils lisent POSTGRES_USER, POSTGRES_DB, POSTGRES_PASSWORD, ENCRYPTION_KEY, APP_PORT et APP_VERSION dans .env, et se replient sur les valeurs par défaut du fichier Compose de référence.
  • Ils lisent les réglages du stockage tels que l’application les voit : un conteneur jetable de l’image app affiche STORAGE_DRIVER, STORAGE_ENDPOINT, STORAGE_REGION, STORAGE_BUCKET, les clés d’accès, STORAGE_FORCE_PATH_STYLE et STORAGE_FS_ROOT comme l’application les lirait. Le fichier Compose peut donc fixer ou remplacer chacun d’eux ; voir Où vivent les blobs.
  • Leurs messages sont en anglais, et la restauration vous demande de taper YES pour confirmer.

Faire une sauvegarde ​

bash
./scripts/backup.sh                  # écrit dans ./backups/<horodatage>
./scripts/backup.sh /mnt/backups     # écrit dans un autre répertoire

L’instance continue de tourner pendant la sauvegarde. Le script :

  1. exporte la base avec pg_dump au format personnalisé de PostgreSQL (compressé, et restaurable en parallèle), écrit directement sur l’hôte ;
  2. copie chaque blob dans blobs/ : chaque objet du bucket avec un stockage S3 (mc mirror), ou le répertoire entier avec STORAGE_DRIVER=fs ;
  3. écrit manifest.json ;
  4. supprime les plus anciennes sauvegardes du répertoire de sortie au-delà de BACKUP_KEEP (7 par défaut ; 0 les garde toutes). Il ne supprime jamais qu’un répertoire qui contient un manifest.json.

Chaque sauvegarde est un répertoire nommé d’après son heure de début en UTC, par exemple 20261004T031500Z :

20261004T031500Z/
├── database.dump
├── blobs/
└── manifest.json

Le répertoire n’est lisible que par son propriétaire : il contient des mails en clair. Traitez-le comme une donnée sensible.

Le script écrit sa progression sur la sortie d’erreur, et seulement le chemin de la nouvelle sauvegarde sur la sortie standard, pour que vous puissiez le réutiliser :

bash
BACKUP=$(./scripts/backup.sh /mnt/backups)
rsync -a "$BACKUP" serveur-de-sauvegarde:/srv/instance-backups/

Planifier les sauvegardes ​

Lancez le script depuis cron, avec un utilisateur autorisé à utiliser Docker. Par exemple, chaque nuit à 3 h 15 :

cron
15 3 * * * cd /srv/instance && ./scripts/backup.sh /mnt/backups >> /var/log/instance-backup.log 2>&1

Copiez ensuite les sauvegardes sur une autre machine ou un autre site : une sauvegarde qui reste sur le disque du serveur ne survit pas à la perte de ce disque.

La durée dépend surtout du nombre d’objets du stockage, plus que de la taille de la base : chaque objet est copié séparément. Mesurez-la sur votre instance, et mesurez-la de nouveau à mesure qu’elle grossit.

Le manifeste ​

manifest.json décrit ce que contient la sauvegarde :

json
{
  "format": 1,
  "startedAt": "2026-10-04T03:15:00Z",
  "durationSeconds": 42,
  "appVersion": "1.4.0",
  "postgresVersion": "16.10",
  "schemaVersion": "0052",
  "database": { "name": "…", "user": "…", "dumpBytes": 104857600 },
  "storage": { "driver": "s3", "bucket": "…", "objects": 48210, "bytes": 2147483648 },
  "counts": { "mailboxes": 12, "messages": 310422 },
  "encryptionKeyFingerprint": "f4ea96b23453dd75"
}
ChampCe qu’il vous dit
schemaVersionLa dernière migration de base appliquée au moment de l’export. Une sauvegarde se restaure dans la même version de l’application ou dans une version plus récente, jamais dans une plus ancienne.
appVersionLa valeur d’APP_VERSION dans .env. Renseignez APP_VERSION dans .env avec la version que vous faites tourner, pour que ce champ ait un sens.
storage.drivers3 ou fs. Une sauvegarde ne se restaure que dans une instance qui utilise le même pilote : les deux rangent leurs fichiers différemment.
countsNombre de boîtes et de messages au moment de la sauvegarde. Comparez-les après une restauration.
encryptionKeyFingerprintUne empreinte courte d’ENCRYPTION_KEY, pas la clé elle-même. La restauration la compare avec la clé de votre .env actuel.

Lisez de temps en temps le manifeste de vos sauvegardes. Un counts.messages à 0 dans un petit export est la sauvegarde parfaitement valide d’une base vide.

Restaurer une sauvegarde ​

DANGER

Une restauration remplace la base actuelle et écrase les blobs : les objets du bucket, ou le répertoire entier avec STORAGE_DRIVER=fs. Ne la lancez jamais sur la production « pour voir » : utilisez une copie.

  1. Vérifiez que .env contient la même ENCRYPTION_KEY qu’au moment de la sauvegarde.

  2. Lancez le script avec le répertoire de la sauvegarde :

    bash
    ./scripts/restore.sh backups/20261004T031500Z
  3. Répondez aux confirmations en tapant YES.

Le script enchaîne ces étapes, dans cet ordre :

  1. Des vérifications, avant toute écriture. L’export, le manifeste et le répertoire blobs/ doivent être présents, et le pilote de stockage de la sauvegarde doit être celui qu’utilise l’application. L’empreinte de clé du manifeste est comparée avec la clé de .env : si elles diffèrent, le script prévient que chaque secret sera illisible et demande une confirmation. Il affiche le nombre de messages de la base actuelle, puis demande une dernière confirmation.
  2. L’arrêt du conteneur applicatif. PostgreSQL est démarré si besoin, et MinIO aussi quand le fichier Compose le porte et que l’application l’utilise.
  3. La recréation de la base (DROP DATABASE … WITH (FORCE), puis CREATE DATABASE) et la restauration de l’export avec pg_restore, quatre tables en parallèle. Il s’arrête à la première erreur.
  4. Le remplissage du stockage. Avec S3 : il crée le bucket s’il manque, le garde privé, et y copie chaque objet de la sauvegarde. Si le fournisseur refuse le réglage de confidentialité, le script prévient et continue : vérifiez la politique du bucket dans la console du fournisseur. Avec fs : il vide le répertoire, puis y copie la sauvegarde.
  5. Le redémarrage de l’application, puis l’attente, jusqu’à 180 secondes, d’une réponse 200 de /readyz. Les migrations plus récentes que la sauvegarde s’appliquent à ce moment-là.
  6. L’affichage de comptages de contrôle : membres, boîtes, fils, messages, workflows, tâches vivantes et tâches mortes. Comparez messages avec counts.messages du manifeste.

Si pg_restore échoue, la base reste partielle : corrigez la cause et relancez le script. RESTORE_YES=1 saute toutes les confirmations ; réservez-la aux tests automatisés, jamais à une restauration tapée à la main.

Après une restauration ​

  • Les sessions sont restaurées elles aussi. Toute personne connectée au moment de la sauvegarde l’est de nouveau. Pour déconnecter tout le monde, supprimez les sessions :

    bash
    docker compose -f compose.reference.yaml exec postgres \
      sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "DELETE FROM sessions;"'
  • La synchronisation reprend depuis l’état enregistré dans la sauvegarde : le miroir rattrape ce qui est arrivé chez les fournisseurs depuis.

  • Le travail postérieur à la sauvegarde est perdu : exécutions, modifications de workflows, tables, réglages.

  • Si vous utilisez un fichier Compose supplémentaire, relancez docker compose up -d avec tous vos fichiers : le script a redémarré l’application avec son seul fichier Compose.

Restaurer sur un nouveau serveur ​

Quand le serveur d’origine a disparu :

  1. Installez Docker et clonez le code source, dans la version de la sauvegarde ou une version plus récente.
  2. Remettez en place le fichier .env que vous avez gardé, avec la même ENCRYPTION_KEY.
  3. Construisez ou récupérez l’image de l’application. Voir Auto-hébergement.
  4. Copiez le répertoire de la sauvegarde sur le serveur et lancez ./scripts/restore.sh <répertoire>.
  5. Pointez le DNS et le reverse proxy vers le nouveau serveur. Si l’adresse publique change, suivez Une nouvelle adresse publique.

Tester une restauration sans toucher à la production ​

Une sauvegarde jamais restaurée n’est pas prouvée. Restaurez-la sur une copie jetable, sur la même machine, avec son propre nom de projet et son propre port :

  1. Copiez .env vers verify.env, et dans la copie réglez :

    ini
    COMPOSE_PROJECT_NAME=instance-verify
    APP_PORT=55480
    PUBLIC_BASE_URL=http://localhost:55480
    SEND_ENABLED=false
    POLL_INTERVAL_SECONDS=86400

    Avec un stockage S3 externe, réglez aussi STORAGE_BUCKET sur un bucket réservé à la vérification, et assurez-vous que votre fichier Compose lit STORAGE_BUCKET dans l’environnement. MinIO et le répertoire fs appartiennent aux volumes de la copie ; un bucket externe non, et la restauration verserait les objets de la sauvegarde dans le bucket de production.

  2. Restaurez dans la copie et vérifiez-la :

    bash
    ENV_FILE=verify.env RESTORE_YES=1 ./scripts/restore.sh backups/20261004T031500Z
    curl -s localhost:55480/readyz
  3. Détruisez la copie et ses volumes :

    bash
    docker compose --env-file verify.env -f compose.reference.yaml down -v

Ne démarrez jamais une copie avec les envois ouverts

Une copie restaurée a les mêmes workflows, les mêmes boîtes et les mêmes secrets que la production. Démarrée telle quelle, elle envoie de vrais mails à de vrais destinataires. SEND_ENABLED=false retient chaque envoi. Il ne retient pas les autres opérations : un workflow de la copie peut encore déplacer, étiqueter ou marquer des mails dans les vraies boîtes. Ne laissez tourner une copie que le temps de la vérification, et assurez-vous que COMPOSE_PROJECT_NAME diffère de celui de la production avant de lancer down -v : avec le même nom de projet, cette commande supprime les volumes de production.

Où vivent les blobs ​

MinIO (fichier Compose de référence) ​

Rien à faire. Les scripts lancent mc dans le réseau du Compose, à travers la tâche createbucket qui porte l’image mc.

Un stockage S3 externe : OVH Object Storage, AWS, Garage, Scaleway… ​

Dans compose.reference.yaml, remplacez les lignes de stockage du bloc environment du service app par celles de votre fournisseur, puis retirez les services minio et createbucket et la dépendance d’app envers createbucket. Pour OVH Object Storage (S3, région Gravelines) :

yaml
      STORAGE_DRIVER: s3
      STORAGE_ENDPOINT: https://s3.gra.io.cloud.ovh.net
      STORAGE_REGION: gra
      STORAGE_BUCKET: ${STORAGE_BUCKET:?}
      STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:?}
      STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:?}
      STORAGE_FORCE_PATH_STYLE: 'false'

Utilisez les clés d’un utilisateur S3 limité à ce bucket, et créez le bucket en privé dans la console du fournisseur. Les scripts n’ont besoin de rien d’autre : sans tâche createbucket, ils lancent l’image mc avec docker run (MC_IMAGE, minio/mc:latest par défaut), qui atteint l’endpoint public. La durée de copie grandit avec le nombre d’objets et la latence vers le fournisseur : lancez la sauvegarde depuis une machine proche du stockage.

Un répertoire : STORAGE_DRIVER=fs ​

Réglez STORAGE_DRIVER=fs dans .env. L’application range les blobs sous STORAGE_FS_ROOT (./.data/blobs par défaut, soit /app/.data/blobs dans l’image), sur le volume app-data que le fichier Compose de référence monte sur /app/.data. MinIO devient alors inutile.

La sauvegarde copie ce répertoire tel quel, avec tar, à travers un conteneur jetable de l’image app qui tourne sous son propre utilisateur ; la restauration vide le répertoire puis le remplit de nouveau. Une sauvegarde prise avec un pilote ne se restaure pas avec l’autre : le script le refuse avant toute écriture. Changer le pilote d’une instance qui a déjà des blobs n’est pas pris en charge : choisissez-le à l’installation.

Restaurer une partie seulement ​

  • Un workflow, un membre, une table. Ne restaurez pas par-dessus la production. Restaurez la sauvegarde sur une copie, comme ci-dessus, et recopiez ce dont vous avez besoin de la copie vers la production.
  • La base sans les blobs, ou l’inverse. Le script restaure toujours les deux. Chacune de ses étapes est une simple commande docker compose : reprenez les commandes de la partie qui vous intéresse.

Ce qu’il faut éviter ​

  • Copier le volume PostgreSQL pendant que la base tourne. Une copie de fichiers d’une base vivante est le plus souvent corrompue, et démarre parfois quand même. pg_dump donne un instantané cohérent.
  • Ranger ENCRYPTION_KEY à côté des sauvegardes. Qui obtient les deux peut déchiffrer l’accès à toutes les boîtes.
  • Restaurer dans une version plus ancienne de l’application. Les migrations ne vont que vers l’avant.
  • Lancer docker compose down -v sur la production. Cette commande supprime les volumes de données.