Homelab Backup Strategy: Restic, Windmill, Garage S3 et PG Back Web
J’ai un homelab qui a pris de plus en plus de place au fil des années : une homepage, un DNS privé / ad-blocker, une to-do list, de la qualité de code, des audits de sécurité… Ce ne sont pas moins de 75 entrées qu’Uptime Kuma monitore au quotidien, le tout réparti sur une dizaine de serveurs.
Pour certains de ces services, la résilience des données est primordiale. Je pense en particulier à Paperless-NGX pour mes documents administratifs, Immich pour mes photos, et surtout Vaultwarden pour mes mots de passe.
Franchement, mettre en place des backups n’est clairement pas la première chose à laquelle on pense. La probabilité de perdre des données reste généralement faible, mais shit happens : un SSD qui rend l’âme, un serveur qui grille à cause d’une surtension, un cambriolage. Et quand ce jour arrive, mieux vaut être préparé, au risque de perdre des données à jamais.
Je partage ici la topologie de mon homelab, la stratégie de backup que j’ai mise en place, les technos impliquées et la manière dont tout ça est orchestré, avant de parler restauration.
Topologie du HomeLab
Il n’y a pas qu’une seule stratégie de backup viable. Tout dépend du niveau de paranoïa, du nombre de services, de serveurs, et du niveau de galère dans lequel tu te retrouverais si tout venait à disparaître demain. Avant de rentrer dans le détail de ma stratégie, voici donc à quoi ressemble mon homelab aujourd’hui.
Mes services tournent sur Docker Compose. Un bon compromis par rapport à des solutions plus lourdes comme k3s ou Docker Swarm, pour lesquelles les problématiques de stockage distribué se posent rapidement et augmentent considérablement l’investissement au quotidien. Ils sont répartis sur une dizaine de serveurs, de manière à minimiser le load moyen de chacun :
| Serveur | Rôle principal |
|---|---|
| server_1 | Reverse proxy, authentification (Traefik, Authelia) |
| server_2 | Monitoring, alerting (Grafana, Uptime Kuma) |
| server_3 | Documents, photos (Paperless-NGX, Immich) |
| server_4 | Dev tools (Forgejo, SonarQube, Windmill) |
| server_5 | Services divers (Vaultwarden, FreshRSS, …) |
| server_6, server_7, server_8 | Nœuds Garage S3 (distants) |
La majorité de ces serveurs sont localisés à mon domicile. Deux exceptions : des serveurs hébergés dans des lieux distants, reliés au réseau local via WireGuard, principalement pour la résilience du cluster Garage S3. On y reviendra.
L’administration de l’ensemble se fait via Ansible : déploiement, mise à jour des stacks, configuration.
Stratégie de backup: vue d’ensemble
Avant de rentrer dans le détail technique, un mot sur la philosophie générale.
Une bonne stratégie de backup repose sur un principe simple : ne jamais avoir qu’une seule copie, ni qu’un seul endroit. Le scénario catastrophe n’est pas forcément un incident majeur: une simple surtension peut suffire à perdre un disque et tout ce qu’il contient.
Dans mon cas, j’ai adopté une approche en couches, du plus critique au plus courant :
- Offline / hors-site : pour les données les plus sensibles, des sauvegardes sur clés USB stockées dans des lieux distincts de mes serveurs. Combiné à un stockage cloud chiffré (Dropbox + AGE, clé répartie sur deux YubiKeys et un paper backup). Je ne détaille pas ce sujet ici, mais c’est le filet de sécurité ultime.
- Network storage : un disque SSD dans mon routeur, accessible en CIFS par tous mes serveurs. Pratique pour des restaurations rapides sur le réseau local.
- Garage S3 : un cluster S3 distribué sur trois nœuds physiquement séparés, communiquant via WireGuard. C’est la destination principale pour la majorité des services, résiliente par design.
Les deux dernières destinations sont celles que je détaille dans cet article.
Backup des configurations et secrets
J’utilise Ansible pour administrer mon homelab. Pour chaque service, j’ai un dossier contenant le docker-compose.yml, le ou les fichiers d’environnement (public et/ou secrets), ainsi que les fichiers de configuration. Un playbook par service permet de déployer ou mettre à jour.
Les fichiers sensibles sont chiffrés via Ansible Vault, ce qui permet de les versionner dans Git sans risque d’exposition. Résultat : configurations et secrets sont dans le dépôt, mais les secrets restent illisibles sans la clé de vault.
Côté hébergement, je stocke mes dépôts dans un Forgejo self-hosted, mirrored automatiquement vers un dépôt GitHub privé. Chaque commit sur Forgejo est ainsi répliqué sur GitHub. Un filet de sécurité supplémentaire si Forgejo venait à être indisponible.
L’avantage de cette approche est simple : les configurations et secrets sont couverts de manière indépendante. On peut donc se concentrer exclusivement sur la sauvegarde des données des services.
Backup des données (volumes Docker)
Restic
Restic est un outil de backup open source, multiplateforme et disponible en Docker. Il supporte plusieurs destinations (disque local, S3) et chiffre les données par défaut. C’est lui qui effectue concrètement les sauvegardes des volumes sur chaque host.
Pour chaque serveur du lab, j’ai un docker-compose.yml dédié à Restic, invoqué par le script Windmill (cf. section suivante) :
$ ls -a /srv/stacks/restic/
. .. docker-compose.yml .env.public .env.secrets
$ cat /srv/stacks/restic/docker-compose.yml
---
services:
restic:
image: restic/restic:latest
container_name: restic
env_file:
- .env.public
- .env.secrets
volumes:
- type: bind
source: /srv/volumes
target: /srv/volumes
read_only: true
- type: bind
source: /mnt/backup/${HOSTNAME}
target: /mnt/backup/${HOSTNAME}
$ cat /srv/stacks/restic/.env.public
RESTIC_REPOSITORY=s3:{{GARAGE_URL}}/{{GARAGE_BUCKET}}/{{HOST}}
HOSTNAME={{HOST}}
$ cat /srv/stacks/restic/.env.secrets
AWS_ACCESS_KEY_ID={{AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY={{AWS_SECRET_ACCESS_KEY}}
RESTIC_PASSWORD={{RESTIC_PASSWORD}}
Quelques précisions sur les variables :
{{GARAGE_URL}}: l’URL complète de Garage, avec le scheme (http://ip:port). Le scheme est obligatoire. Sans lui, Restic tente du HTTPS et échoue silencieusement.{{HOST}}: le hostname du serveur. Utilisé pour organiser les backups en sous-dossiers dédiés, aussi bien sur S3 que sur le network storage.{{AWS_ACCESS_KEY_ID}}et{{AWS_SECRET_ACCESS_KEY}}: les credentials créés dans Garage (détaillés dans la section dédiée).{{GARAGE_BUCKET}}: le nom du bucket de destination.{{RESTIC_PASSWORD}}: le mot de passe de chiffrement du repo Restic. À conserver précieusement. Sans lui, les données sauvegardées sont irrécupérables.
Windmill
Windmill est une plateforme de scripts et workflows low-code, self-hostable. Les scripts peuvent être écrits dans de nombreux langages (Python, Bash, Go, Rust, Java…) et déclenchés via un scheduler, un appel HTTP, ou par email.
C’est lui qui orchestre l’ensemble du processus de backup : il décide quoi sauvegarder, où, et quand.
Configuration des services à sauvegarder
Pour chaque serveur, j’ai créé dans Windmill une variable contenant un fichier YAML décrivant les services à sauvegarder :
services:
service_1:
targets:
- s3
- local
src:
- /srv/volumes/service_1
needs_restart: false
service_2:
targets:
- local
src:
- /srv/volumes/service_2
excludes:
- /srv/volumes/service_2/sub_folder
needs_restart: true
docker:
compose_path: /srv/stacks/service_2/docker-compose.yml
Dans cet exemple :
service_1est sauvegardé vers S3 (Garage) et en local (network storage), sans interruption de service.service_2est sauvegardé uniquement en local, en excluant un sous-dossier spécifique. Il nécessite un arrêt préalable (needs_restart: true). Le chemin dudocker-compose.ymlest alors obligatoire pour que le script puisse stopper et redémarrer la stack.
La rétention est également configurable par service :
service_1:
targets:
- s3
src:
- /srv/volumes/service_1
retention:
daily: 7
weekly: 4
monthly: 6
Sans configuration explicite, les valeurs par défaut s’appliquent : 7 jours, 4 semaines, 6 mois. Restic applique automatiquement cette politique via forget --prune après chaque backup.
Le script
Le script prend trois paramètres en entrée : le host cible, une liste de services (ou all pour tout sélectionner), et le mode de destination (local, s3, ou all).
À chaque exécution, généralement déclenchée par un scheduler quotidien, il :
- Charge la configuration YAML du host cible.
- Pour chaque service à sauvegarder, se connecte en SSH au host avec le user
windmillet la clé dédiée. - Initialise le repo Restic si ce n’est pas déjà fait (
restic init), et déverrouille un éventuel lock résiduel (restic unlock). - Exécute le backup via
docker compose run restic backup. - Applique la politique de rétention via
restic forget --prune. - En cas d’échec sur un service marqué
needs_restart, tente un redémarrage d’urgence de la stack. - Envoie une notification Ntfy en fin d’exécution, récapitulant les succès et les éventuelles erreurs.

Volumes de destination
Network storage
J’ai un disque SSD dans mon routeur, exposé en CIFS sur le réseau local. C’est lui qui sert de destination locale pour les backups Restic.
Le disque est monté avec le nom disk1. Pour le rendre accessible à chaque serveur, j’ajoute la ligne suivante dans /etc/fstab :
//<router-ip>/disk1/backup /mnt/backup cifs guest,uid=1000,gid=1000,file_mode=0644,x-systemd.automount,x-systemd.idle-timeout=60,_netdev 0 0
Puis on crée le point de montage, on recharge systemd et on monte :
sudo mkdir -p /mnt/backup
sudo systemctl daemon-reload
sudo mount /mnt/backup
Pour vérifier que le montage est actif :
findmnt /mnt/backup
Output selon le cas :
# Montage automatique (automount) :
TARGET SOURCE FSTYPE OPTIONS
/mnt/backup systemd-1 autofs rw,relatime,...
# Montage manuel (via mount) :
TARGET SOURCE FSTYPE OPTIONS
/mnt/backup //<router-ip>/disk1/backup cifs rw,relatime,vers=3.1.1,...
Ou plus simplement : créer un fichier depuis un host et vérifier qu’il apparaît depuis un second.
Garage S3
Garage est une solution de stockage distribué, compatible S3, pensée pour le homelab. Je l’ai choisi en remplacement de Minio, dont les changements de licence successifs et le désengagement progressif de la communauté open source m’ont convaincu de chercher une alternative. Garage est à l’opposé : porté par une association communautaire française, sans velléité commerciale.
Mon cluster est constitué de trois nœuds: des Raspberry Pi 5 avec hat SSD (512 Go chacun), répartis dans trois lieux différents et reliés via WireGuard. Chaque SSD a deux partitions : 32 Go pour l’OS, le reste pour Garage. L’idée est de garantir que les données restent accessibles même en cas d’incident sur l’un des sites ou d’une coupure réseau sur l’un des nœuds.
Le docker-compose.yml de chaque nœud :
---
services:
garage:
image: dxflrs/garage:v2.2.0
container_name: garage
restart: always
network_mode: host
volumes:
- type: bind
source: /srv/config/garage/garage.toml
target: /etc/garage.toml
- type: bind
source: /mnt/garage/meta
target: /var/lib/garage/meta
- type: bind
source: /mnt/garage/data
target: /var/lib/garage/data
- type: bind
source: /mnt/garage/db
target: /var/lib/garage/db
deploy:
resources:
limits:
cpus: "1"
memory: 512m
reservations:
memory: 128m
Le SSD est monté via fstab :
UUID=XXX /mnt/garage ext4 defaults,noatime 0 2
Je ne détaille pas ici la configuration de garage.toml ni l’initialisation du cluster. Ce n’est pas l’objet de cet article. La documentation officielle couvre ça très bien.
Créer le bucket et les credentials
Une fois le cluster initialisé, on crée un bucket dédié aux backups :
$ docker exec garage /garage bucket create backup-bucket
==== BUCKET INFORMATION ====
Bucket: 4a663444...
Global alias: backup-bucket
Puis une clé d’accès :
$ docker exec garage /garage key create backup-key
==== ACCESS KEY INFORMATION ====
Key ID: GKdb6e91673a66448bc019c7d1
Secret key: 187...
Ces deux valeurs correspondent respectivement à {{AWS_ACCESS_KEY_ID}} et {{AWS_SECRET_ACCESS_KEY}} dans la config Restic.
Il reste à donner à cette clé les droits lecture/écriture sur le bucket :
$ docker exec garage /garage bucket allow --read --write backup-bucket --key backup-key
==== KEYS FOR THIS BUCKET ====
Permissions Access key Local aliases
RW GKdb6e91673a66448bc019c7d1 backup-key
Préparation des hosts
À ce stade, Restic est configuré sur chaque host, le network storage est monté, et Garage est prêt à recevoir des données. Il reste à créer le liant entre les hosts et Windmill : un user dédié et une clé SSH que le script utilisera pour se connecter.
Créer le user windmill
sudo useradd -m -d /var/lib/windmill -s /bin/bash windmill
Générer la clé SSH
ssh-keygen -t ed25519 -C "windmill" -f windmill_key
Cela génère deux fichiers : windmill_key (clé privée) et windmill_key.pub (clé publique).
Déployer la clé publique sur le host
sudo mkdir /var/lib/windmill/.ssh
sudo mv windmill_key.pub /var/lib/windmill/.ssh/authorized_keys
sudo chown -R windmill:windmill /var/lib/windmill/.ssh
sudo chmod 700 /var/lib/windmill/.ssh
sudo chmod 600 /var/lib/windmill/.ssh/authorized_keys
Donner à windmill les droits Docker
Le script doit pouvoir exécuter docker compose down/up pour les services marqués needs_restart: true. Pour ça, le user windmill doit appartenir au groupe docker :
sudo usermod -aG docker windmill
Vérifier la connexion
ssh -i windmill_key windmill@<host-ip>
Si la connexion s’établit, le setup est correct.
Finaliser
Ajoutez la clé privée (windmill_key) en tant que secret dans Windmill, puis supprimez-la du host :
rm windmill_key
Comment ça marche ?
Pour récapituler, voilà ce qui se passe concrètement quand un backup se déclenche.
Un scheduler Windmill trigger quotidiennement le script de backup pour chaque host. Le script commence par charger la variable YAML correspondant au host cible. C’est elle qui définit quels services sauvegarder, vers quelles destinations, et lesquels nécessitent un arrêt préalable.
Pour chaque service listé :
- Si
needs_restart: true, la stack est stoppée viadocker compose down. - Le script se connecte en SSH au host (user
windmill, clé dédiée) et invoque Restic pour sauvegarder les volumes sources vers les destinations configurées: S3 (Garage), local (network storage), ou les deux. - Restic initialise le repo si nécessaire, déverrouille un éventuel lock résiduel, effectue le backup, puis applique la politique de rétention (
forget --prune). - Si le service a été stoppé, il est redémarré via
docker compose up -d. En cas d’échec du backup, un redémarrage d’urgence est tenté pour ne pas laisser le service down.
Une fois tous les services traités, une notification Ntfy est envoyée avec le récapitulatif : ce qui a été sauvegardé avec succès, et ce qui a éventuellement échoué.
Backup des bases de données
J’ai dans ma stack plusieurs solutions de stockage de données : MariaDB, MongoDB, SQLite, Redis, et Postgres. J’ai fait le choix, dans la mesure du possible, de privilégier Postgres pour les services qui le supportent. Ca permet de centraliser la stratégie de backup sur un seul outil.
Quelques cas particuliers :
- Redis : uniquement utilisé comme cache dans ma stack. Pas de backup nécessaire, mais attention, Redis peut aussi servir de base de données NoSQL à part entière. Vérifiez l’usage qui en est fait dans vos services avant de l’exclure.
- MariaDB, MongoDB, SQLite : pour ces bases, j’ai fait le choix de stopper le service, de sauvegarder les bind mounts via Restic (comme n’importe quel volume), puis de redémarrer. C’est acceptable quand les données ne sont pas critiques et que le service peut être indisponible quelques minutes. Dans le cas contraire, une solution de backup dédiée s’impose.
Pour Postgres, j’utilise PG Back Web.
La configuration se fait entièrement depuis l’interface :
- Databases : une entrée par base à sauvegarder, avec l’URI classique (
postgres://user:password@host:port/database_name). - Destinations : S3 (Garage) ou disque local via bind mount dans le docker-compose de PG Back Web.
- Backup Tasks : une tâche par base, avec la base cible, la destination, le schedule (expression cron), la durée de rétention, et diverses options pg_dump (
--data-only,--schema-only,--clean,--if-exists,--create,--no-comments).
Les exécutions passées sont visibles sous l’onglet Executions. PG Back Web propose également un onglet Webhooks pour notifier le succès ou l’échec des backups. Via Slack, Ntfy, ou tout autre endpoint HTTP.
L’importance du test des backups
Vos services sont maintenant sauvegardés régulièrement, à différents endroits, avec une haute résilience. Manque de pot : le jour où vous avez besoin de restaurer, vous vous rendez compte que les sauvegardes sont vides, que la clé de chiffrement a disparu, ou que les données sont corrompues. Tout ce process, pour rien.
C’est une réalité : une stratégie de backup est rarement mise en place. Une stratégie résiliente, encore moins. Et le test de restauration est encore plus rare.
Mon conseil : au moment où vous mettez en place vos backups, définissez aussi votre procédure de restauration. Et testez-la de temps en temps. La fréquence dépend de la criticité des données et de l’effort que vous êtes prêt à y consacrer. Une fois par trimestre pour les services critiques est un bon point de départ.
Restauration des backups
Avoir des backups, c’est bien. Savoir les restaurer avant d’en avoir besoin, c’est mieux. Voici les procédures concrètes pour Restic et PG Back Web.
Restic
La restauration se fait en deux étapes : identifier le snapshot à restaurer, puis le restaurer.
Lister les snapshots disponibles
docker compose -f /srv/stacks/restic/docker-compose.yml run --rm \
-e "RESTIC_REPOSITORY=s3:http://<garage-url>/<bucket>/<host>/<service>" \
restic snapshots
Output typique :
ID Time Host Paths Size
------------------------------------------------------------------------------
ddb07e93 2026-03-28 14:42:00 server_1 /srv/volumes/changedetection 2.255 MiB
52010d52 2026-03-29 01:05:59 server_1 /srv/volumes/changedetection 2.323 MiB
56b95dec 2026-04-17 23:30:06 server_1 /srv/volumes/changedetection 5.649 MiB
------------------------------------------------------------------------------
11 snapshots
Chaque service a son propre repo Restic, un par dossier dans le bucket S3, et un par dossier dans le network storage. Il faut donc spécifier le service dans l’URL du repo.
Restaurer à l’emplacement d’origine
# 1. Stopper le service
cd /srv/stacks/<service> && docker compose down
# 2. Restaurer le snapshot le plus récent
docker compose -f /srv/stacks/restic/docker-compose.yml run --rm \
-e "RESTIC_REPOSITORY=s3:http://<garage-url>/<bucket>/<host>/<service>" \
-v /srv/volumes:/srv/volumes \
restic restore latest --target /
# 3. Redémarrer
docker compose up -d
Pour restaurer un snapshot spécifique, remplacer latest par l’ID du snapshot (ex: 52010d52).
Inspecter avant d’écraser
Si vous voulez vérifier le contenu d’un snapshot avant d’écraser les données en place :
docker compose -f /srv/stacks/restic/docker-compose.yml run --rm \
-e "RESTIC_REPOSITORY=s3:http://<garage-url>/<bucket>/<host>/<service>" \
-v /tmp/restore:/tmp/restore \
restic restore latest --target /tmp/restore
PG Back Web
La restauration d’un dump Postgres se fait depuis l’onglet Executions de l’interface PG Back Web. Chaque exécution listée dispose d’un bouton de restauration qui rejoue le dump sur la base cible.
Pour une restauration manuelle depuis un dump .sql récupéré sur le disque ou le S3 :
psql postgres://user:password@host:port/database_name < dump.sql
Conclusion : et si tout disparaît ?

Vous avez maintenant une stratégie de backup solide, des données sauvegardées à plusieurs endroits, et une procédure de restauration testée. Mais il reste un dernier angle mort, souvent négligé : pour accéder à vos backups, vous avez besoin de secrets. Et ces secrets doivent survivre à la disparition de votre homelab.
Concrètement, dans le setup décrit ici, trois éléments sont indispensables pour reconstruire from scratch :
- Les credentials Garage (AWS key ID et secret): pour accéder au cluster S3 et lire les snapshots Restic.
- Le mot de passe Restic: pour déchiffrer les données sauvegardées.
- La clé Ansible Vault: pour déchiffrer les configurations et secrets des services versionnés sur Forgejo/GitHub.
Sans l’un de ces trois éléments, vos backups sont inutilisables.
Assurez-vous que ces secrets sont stockés hors de votre homelab : dans votre gestionnaire de mots de passe (lui-même sauvegardé, cf. Vaultwarden + Restic 🙂), sur une clé USB hors-site, ou dans votre cloud chiffré. Un paper backup dans un endroit sûr n’est pas une mauvaise idée non plus.
Un backup sans accès aux clés de déchiffrement, c’est comme un coffre-fort dont vous avez perdu le code. Les données sont là, intactes et totalement inaccessibles.
Ressources
- Script de backup Windmill : [https://gist.github.com/letrome/b1c21e2d4b0fb03732f59dc60d28ee78](GitHub Gist)
- Restic : restic.net — documentation officielle
- Garage S3 : garagehq.deuxfleurs.fr — documentation et quick start
- Windmill : windmill.dev — documentation officielle
- PG Back Web : github.com/eduardolat/pgbackweb — dépôt GitHub