Français
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 vit | Dans la sauvegarde de backup.sh | |
|---|---|---|
| L’état : membres, boîtes, workflows, exécutions, file de tâches, tables, sessions, secrets chiffrés | PostgreSQL | Oui : database.dump |
| Corps de mails, pièces jointes, fichiers ajoutés aux exécutions | Stockage des blobs : un bucket S3, ou un répertoire avec STORAGE_DRIVER=fs | Oui : blobs/ |
ENCRYPTION_KEY | Votre fichier .env | Non, 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 composev2. - Ils lisent le fichier
.envet le fichier Compose à la racine du dépôt. UtilisezENV_FILEetCOMPOSE_FILEpour désigner d’autres chemins. - Ils lisent
POSTGRES_USER,POSTGRES_DB,POSTGRES_PASSWORD,ENCRYPTION_KEY,APP_PORTetAPP_VERSIONdans.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
appafficheSTORAGE_DRIVER,STORAGE_ENDPOINT,STORAGE_REGION,STORAGE_BUCKET, les clés d’accès,STORAGE_FORCE_PATH_STYLEetSTORAGE_FS_ROOTcomme 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
YESpour confirmer.
Faire une sauvegarde
bash
./scripts/backup.sh # écrit dans ./backups/<horodatage>
./scripts/backup.sh /mnt/backups # écrit dans un autre répertoireL’instance continue de tourner pendant la sauvegarde. Le script :
- exporte la base avec
pg_dumpau format personnalisé de PostgreSQL (compressé, et restaurable en parallèle), écrit directement sur l’hôte ; - copie chaque blob dans
blobs/: chaque objet du bucket avec un stockage S3 (mc mirror), ou le répertoire entier avecSTORAGE_DRIVER=fs; - écrit
manifest.json; - supprime les plus anciennes sauvegardes du répertoire de sortie au-delà de
BACKUP_KEEP(7 par défaut ;0les garde toutes). Il ne supprime jamais qu’un répertoire qui contient unmanifest.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.jsonLe 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>&1Copiez 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"
}| Champ | Ce qu’il vous dit |
|---|---|
schemaVersion | La 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. |
appVersion | La 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.driver | s3 ou fs. Une sauvegarde ne se restaure que dans une instance qui utilise le même pilote : les deux rangent leurs fichiers différemment. |
counts | Nombre de boîtes et de messages au moment de la sauvegarde. Comparez-les après une restauration. |
encryptionKeyFingerprint | Une 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.
Vérifiez que
.envcontient la mêmeENCRYPTION_KEYqu’au moment de la sauvegarde.Lancez le script avec le répertoire de la sauvegarde :
bash./scripts/restore.sh backups/20261004T031500ZRépondez aux confirmations en tapant
YES.
Le script enchaîne ces étapes, dans cet ordre :
- 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. - 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.
- La recréation de la base (
DROP DATABASE … WITH (FORCE), puisCREATE DATABASE) et la restauration de l’export avecpg_restore, quatre tables en parallèle. Il s’arrête à la première erreur. - 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. - Le redémarrage de l’application, puis l’attente, jusqu’à 180 secondes, d’une réponse
200de/readyz. Les migrations plus récentes que la sauvegarde s’appliquent à ce moment-là. - L’affichage de comptages de contrôle : membres, boîtes, fils, messages, workflows, tâches vivantes et tâches mortes. Comparez
messagesaveccounts.messagesdu 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 :
bashdocker 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 -davec 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 :
- Installez Docker et clonez le code source, dans la version de la sauvegarde ou une version plus récente.
- Remettez en place le fichier
.envque vous avez gardé, avec la mêmeENCRYPTION_KEY. - Construisez ou récupérez l’image de l’application. Voir Auto-hébergement.
- Copiez le répertoire de la sauvegarde sur le serveur et lancez
./scripts/restore.sh <répertoire>. - 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 :
Copiez
.envversverify.env, et dans la copie réglez :iniCOMPOSE_PROJECT_NAME=instance-verify APP_PORT=55480 PUBLIC_BASE_URL=http://localhost:55480 SEND_ENABLED=false POLL_INTERVAL_SECONDS=86400Avec un stockage S3 externe, réglez aussi
STORAGE_BUCKETsur un bucket réservé à la vérification, et assurez-vous que votre fichier Compose litSTORAGE_BUCKETdans l’environnement. MinIO et le répertoirefsappartiennent aux volumes de la copie ; un bucket externe non, et la restauration verserait les objets de la sauvegarde dans le bucket de production.Restaurez dans la copie et vérifiez-la :
bashENV_FILE=verify.env RESTORE_YES=1 ./scripts/restore.sh backups/20261004T031500Z curl -s localhost:55480/readyzDétruisez la copie et ses volumes :
bashdocker 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_dumpdonne 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 -vsur la production. Cette commande supprime les volumes de données.