tva
← Insights

Reprise après sinistre pour les services auto-hébergés : notre stratégie de sauvegarde

L'auto-hébergement vous donne un contrôle que les services gérés n'offrent pas : vous choisissez le matériel, le réseau, la résidence des données et la structure des coûts. Mais en réalité, ce contrôle s'accompagne d'une responsabilité que les services gérés assument discrètement : lorsque quelque chose tourne mal, c'est entièrement votre problème. Pour une configuration sur un seul serveur exécutant plusieurs services en productionSupabase, un CRM, un DAM, un serveur Git – la stratégie de sauvegarde et de récupération n'est pas une préoccupation secondaire. C'est la différence entre un incident récupérable et une perte de données permanente.

Cet article documente la stratégie de sauvegarde que nous utilisons pour un environnement auto-hébergé en production. Les services impliqués sont une pile Supabase (PostgreSQL comme magasin de données principal), un CRM auto-hébergé (Twenty, qui utilise également PostgreSQL), un serveur Git Gitea, et un système de gestion des actifs numériques. Tous fonctionnent comme des conteneurs Docker sur un seul VPS Hetzner. La stratégie doit tenir compte de tous sans créer des fenêtres de charge I/O inacceptables ou des dépendances d'orchestration complexes.

Pourquoi un serveur unique augmente les enjeux

Une configuration sur un seul serveur n'a aucune redondance intégrée. Il n'y a pas de réplique en attente, pas de basculement multi-zone de disponibilité, pas de restauration automatique de base de données déclenchée par un contrôle de santé. Lorsque le serveur tombe en panne – que ce soit en raison d'une défaillance matérielle, d'une mise à niveau ratée, d'un problème de stockage ou d'un incident de sécurité – le seul chemin de récupération est une sauvegarde. Si la sauvegarde est incomplète, périmée ou non testée, le chemin de récupération est limité en conséquence.

Il s'agit d'un modèle de menace différent de celui d'un service cloud géré, où la défaillance matérielle est largement invisible car le fournisseur d'infrastructure gère le basculement. Sur un seul VPS, vous planifiez explicitement les modes de défaillance que les services gérés abstraient. La planification n'est pas particulièrement complexe, mais elle nécessite de le faire avant l'incident plutôt que pendant.

Ce qui doit être sauvegardé

Chaque service a son propre profil de données. Comprendre quelles données sont irremplaçables et où elles résident est la condition préalable à la conception de la stratégie de sauvegarde.

Supabase stocke ses données dans une instance PostgreSQL gérée par la pile Supabase. La base de données contient toutes les données d'application, les enregistrements des utilisateurs et les métadonnées d'authentification et de stockage de Supabase elle-même. Les buckets de stockage (fichiers téléchargés) sont stockés sur disque et nécessitent un traitement séparé de la sauvegarde de la base de données.

Twenty CRM est soutenu par sa propre instance PostgreSQL, séparée de celle de Supabase. Il contient des enregistrements de contacts, des données d'opportunités et l'état des workflows. Cette base de données est plus petite en volume mais très sensible – perdre des données CRM est plus perturbateur opérationnellement que perdre des données d'application qui peuvent être recréées.

Gitea stocke les dépôts sur disque comme des objets Git nus. Les données du dépôt sont en principe reconstructibles à partir des postes de travail des développeurs (puisque chaque clone est une sauvegarde complète), mais les données du suivi des tickets, les commentaires de pull requests et la configuration de l'équipe ne vivent que dans la base de données de Gitea et ne sont présents dans aucun clone. Les objets Git et la base de données Gitea doivent tous deux être sauvegardés.

DAM (Système de gestion des actifs numériques) stocke les fichiers originaux, les dérivés traités et les métadonnées. Les fichiers originaux sont la partie irremplaçable ; les dérivés peuvent être régénérés. Les métadonnées résident dans une base de données qui enregistre les relations entre fichiers, les tags et les droits d'utilisation.

Orchestration de pg_dump

Le pg_dump de PostgreSQL est le bon outil pour les sauvegardes logiques de base de données. Il exporte une représentation SQL de la base de données qui peut être restaurée vers n'importe quelle version compatible de PostgreSQL, ce qui est plus portable que les sauvegardes physiques (qui nécessitent la même version de PostgreSQL et la même disposition binaire).

Le script de sauvegarde pour chaque instance PostgreSQL suit le même modèle :

#\!/usr/bin/env bash
set -euo pipefail

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="$1"
CONTAINER_NAME="$2"
BACKUP_DIR="/opt/backups/postgres"
OUTPUT_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"

mkdir -p "${BACKUP_DIR}"

docker exec "${CONTAINER_NAME}"   pg_dump -U postgres -d "${DB_NAME}"   | gzip -9 > "${OUTPUT_FILE}"

echo "Sauvegarde terminée : ${OUTPUT_FILE} ($(du -sh "${OUTPUT_FILE}" | cut -f1))"

Le set -euo pipefail au début est important : il fait quitter le script immédiatement si une commande échoue, y compris les commandes dans un pipeline. Sans pipefail, un pg_dump échoué suivi d'un gzip réussi produirait un fichier compressé contenant un message d'erreur, que la tentative de restauration traiterait comme une sauvegarde corrompue.

La sortie est compressée avec gzip -9 en ligne plutôt qu'en post-traitement. Cela maintient l'empreinte disque petite et évite d'écrire d'abord un dump non compressé sur disque, ce qui importe sur des serveurs où l'espace libre est une contrainte gérée.

Planification décalée

Exécuter tous les jobs de sauvegarde simultanément créerait une fenêtre de conflit d'E/S qui pourrait dégrader les services sauvegardés. Une instance Supabase sous charge ne bénéficie pas de se voir en concurrence avec un pg_dump pour les E/S disque. La solution est la planification décalée – chaque job de sauvegarde commence à un moment différent, avec un espacement suffisant pour permettre à la tâche précédente de se terminer avant le début de la suivante.

Le planning que nous utilisons, exprimé en entrées cron :

# Sauvegarde PostgreSQL Supabase — quotidien à 02h00
0 2 * * * /opt/scripts/pg-backup.sh supabase supabase-db

# Sauvegarde PostgreSQL Twenty CRM — quotidien à 02h30
30 2 * * * /opt/scripts/pg-backup.sh twenty twenty-db

# Sauvegarde base de données Gitea — quotidien à 03h00
0 3 * * * /opt/scripts/pg-backup.sh gitea gitea-db

# Sauvegarde base de données DAM — quotidien à 03h30
30 3 * * * /opt/scripts/pg-backup.sh dam dam-db

# Objets de dépôt Gitea — quotidien à 04h00
0 4 * * * /opt/scripts/git-objects-backup.sh

# Fichiers de stockage (Supabase + originaux DAM) — quotidien à 04h30
30 4 * * * /opt/scripts/files-backup.sh

Les intervalles de 30 minutes entre les jobs sont conservateurs – la plupart des sauvegardes de bases de données se terminent en quelques minutes pour des bases de données de cette taille. Mais les intervalles tiennent également compte de l'étape de téléchargement qui suit chaque sauvegarde : le fichier compressé est téléchargé vers le stockage objet avant que le prochain job commence, de sorte que le disque local n'accumule pas plusieurs jours de sauvegardes simultanément.

Téléchargement vers le stockage objet et rétention

Les sauvegardes qui n'existent que sur le même serveur que les services qu'elles protègent ne sont pas des sauvegardes – ce sont des instantanés qui seront perdus dans le même incident qui détruit les données qu'ils protègent. Chaque sauvegarde doit être téléchargée vers un emplacement de stockage séparé avant que la copie locale puisse être considérée comme complète.

Nous utilisons un fournisseur de stockage objet compatible S3 (séparé du fournisseur VPS) et téléchargeons avec rclone, qui gère automatiquement les nouvelles tentatives, les transferts reprenables et la vérification :

rclone copy "${OUTPUT_FILE}" "backup-remote:tva-backups/postgres/${DB_NAME}/"   --checksum   --transfers 1   --log-level INFO

L'indicateur --checksum vérifie le transfert en utilisant les checksums MD5 plutôt que simplement le temps de modification et la taille, ce qui détecte toute corruption pendant le transfert.

La rétention est appliquée par un job de nettoyage séparé qui s'exécute hebdomadairement. La politique de rétention est : sauvegardes quotidiennes conservées pendant sept jours, sauvegardes hebdomadaires (sauvegarde du dimanche, renommée par un job hebdomadaire) conservées pendant quatre semaines, sauvegardes mensuelles (premier dimanche de chaque mois) conservées pendant six mois. Cela donne une fenêtre raisonnable pour détecter des pertes de données qui ne sont pas immédiatement évidentes – une ligne corrompue introduite il y a trois semaines peut encore être récupérée à partir d'une sauvegarde hebdomadaire.

Le nettoyage de rétention utilise rclone delete avec un filtre sur le temps de modification plutôt que de supprimer par modèle de nom de fichier, ce qui est plus fiable lorsque les conventions de nommage des fichiers ne sont pas parfaitement cohérentes :

rclone delete "backup-remote:tva-backups/postgres/"   --min-age 7d   --include "*_daily_*"   --dry-run

L'indicateur --dry-run est utilisé lors du test du job de nettoyage. Supprimez-le uniquement après avoir confirmé que les modèles de filtres correspondent exactement à ce qui doit être supprimé.

Tests de restauration

Une sauvegarde qui n'a jamais été testée est une hypothèse. La seule façon de savoir qu'une sauvegarde est restaurable est de la restaurer. Nous exécutons un test de restauration mensuel pour chaque base de données, en utilisant un conteneur Docker temporaire isolé de la pile de production :

#\!/usr/bin/env bash
set -euo pipefail

BACKUP_FILE="$1"
TEST_CONTAINER="restore-test-$(date +%s)"

# Démarrer un conteneur PostgreSQL temporaire
docker run -d   --name "${TEST_CONTAINER}"   -e POSTGRES_PASSWORD=testpass   -e POSTGRES_DB=testdb   postgres:15-alpine

# Attendre que PostgreSQL soit prêt
sleep 5

# Restaurer la sauvegarde
zcat "${BACKUP_FILE}" | docker exec -i "${TEST_CONTAINER}"   psql -U postgres -d testdb

# Vérifier que les comptages de lignes correspondent aux attentes
docker exec "${TEST_CONTAINER}"   psql -U postgres -d testdb   -c "SELECT schemaname, tablename, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;"

# Nettoyage
docker stop "${TEST_CONTAINER}" && docker rm "${TEST_CONTAINER}"

La vérification du comptage de lignes n'est pas exhaustive – elle vérifie que les tables principales ont des comptages de lignes plausibles, pas que chaque ligne est correcte. Un test plus approfondi exécuterait les propres contrôles de santé de l'application sur la base de données restaurée, mais la vérification du comptage de lignes détecte les modes de défaillance les plus courants : une sauvegarde tronquée, une restauration échouée qui a produit une base de données vide, ou une incompatibilité de version qui a causé une perte silencieuse de données.

À quoi ressemble la récupération réelle

La stratégie de sauvegarde n'est utile que dans la mesure où la procédure de récupération qui l'utilise l'est. Le runbook de récupération – écrit comme un document Markdown dans le dépôt de configuration du serveur – documente la séquence exacte d'étapes pour restaurer chaque service sur un nouveau VPS.

La séquence pour une défaillance complète du serveur est : provisionner un nouveau VPS, installer Docker et la pile d'application depuis le dépôt de configuration, télécharger les fichiers de sauvegarde les plus récents depuis le stockage objet, restaurer chaque base de données dans l'ordre des dépendances (Supabase en premier, puis CRM, puis les autres), restaurer le stockage de fichiers, mettre à jour le DNS, et vérifier chaque service. Le temps estimé pour une récupération complète est inférieur à deux heures si toutes les sauvegardes sont à jour et si le runbook est suivi sans improvisation.

Le mot clé dans cette estimation est “suivi sans improvisation.” Le runbook devrait être suffisamment spécifique pour qu'une personne n'ayant jamais touché au système puisse l'exécuter avec succès. Les commandes devraient être copiables-collables, pas décrites en prose. Chaque étape devrait avoir une vérification. L'ambiguïté dans un runbook de récupération est une responsabilité qui se multiplie sous le stress d'un incident réel.

Nous testons le runbook annuellement en effectuant une récupération complète vers un environnement de staging. Le test met de manière fiable en évidence au moins une étape qui a changé depuis la dernière rédaction du runbook – une version d'image Docker qui a bougé, une clé de configuration renommée, une variable d'environnement ajoutée. Détecter ces problèmes lors d'un test planifié est de loin préférable à les découvrir lors d'une vraie récupération.

Ressources connexes

Articles connexes