Backups, die man auch zurückspielen kann: restic im Homelab

Noah Kleinert
backupslinuxdockerself-hosting

Seit Fotos, Dokumente und Passwörter bei mir im Homelab liegen, ist eine Frage plötzlich ziemlich wichtig geworden: Was passiert, wenn eine Festplatte stirbt? Oder ich aus Versehen das falsche Verzeichnis lösche? Oder die Wohnung abbrennt?

In meinem Homelab-Update habe ich kurz erwähnt, dass ich dafür restic nutze. In diesem Beitrag zeige ich, wie das genau aufgebaut ist, mit allen Befehlen zum Nachmachen.


Die 3-2-1-Regel

Die bekannteste Faustregel für Backups ist 3-2-1:

  • 3 Kopien der Daten (das Original zählt mit)
  • auf 2 verschiedenen Medien
  • davon 1 außer Haus

Bei mir sieht das so aus:

  1. Original: auf der Docker-VM bzw. dem Raspberry Pi 5
  2. Lokale Kopie: auf meinem alten ReadyNAS, eingebunden per NFS. Davon kann ich schnell wiederherstellen, ohne erst alles aus dem Internet zu laden.
  3. Offsite-Kopie: auf einer Hetzner Storage Box (1 TB), per SFTP. Die schützt gegen Brand, Diebstahl oder Überspannung, also alles, was beide Geräte zu Hause auf einmal erwischt.

Ein RAID ist übrigens kein Backup. Wenn ich eine Datei lösche, löscht das RAID sie brav auf beiden Platten gleichzeitig.


Warum restic?

Ich habe mir ein paar Tools angeschaut (BorgBackup, Duplicati, Kopia) und bin bei restic gelandet, weil:

  • alles clientseitig verschlüsselt wird. Hetzner sieht nur Datenmüll, nicht meine Fotos.
  • es dedupliziert. Jede Nacht wird ein kompletter Stand gesichert („Snapshot“), aber nur die geänderten Teile werden wirklich übertragen. Das spart enorm viel Platz.
  • es ein einzelnes Programm ohne Server ist und direkt SFTP kann. Auf der Storage Box muss ich also nichts installieren.
  • die Doku sehr gut ist und es seit Jahren stabil läuft.

Installiert ist es schnell:

bash
sudo apt install restic

Ein Repository anlegen

restic speichert alles in einem sogenannten Repository. Ich habe pro Host und pro Ziel eins, also z. B. für die Docker-VM eins auf dem NAS und eins auf der Storage Box.

Das Passwort für das Repository liegt in einer Datei, damit restic nicht jedes Mal fragt:

bash
sudo install -d -m 0700 /etc/restic
openssl rand -hex 48 | sudo tee /etc/restic/password > /dev/null
sudo chmod 600 /etc/restic/password

Bei mir legt Ansible diese Datei aus einem verschlüsselten Secret im Repo an (wie das funktioniert, steht in meinem Beitrag über SOPS und age). Ganz wichtig: Dieses Passwort zusätzlich an einem sicheren Ort aufheben (Passwortmanager und offline). Ohne das Passwort sind die Backups wertlos, da hilft auch Hetzner nicht weiter.

Dann das lokale Repository auf dem NFS-Share anlegen:

bash
sudo restic -r /mnt/backup/restic/docker-vm --password-file /etc/restic/password init

Die Hetzner Storage Box per SFTP

Bei der Storage Box habe ich für jeden Host einen eigenen Sub-Account angelegt. Jeder Sub-Account sieht nur sein eigenes Verzeichnis. Falls also mal ein Server kompromittiert wird, kommt der Angreifer nicht an die Backups der anderen.

Damit restic sich per SSH-Key anmelden kann, bekommt root einen Eintrag in der SSH-Konfiguration:

text
# /root/.ssh/config
Host storagebox
  HostName u123456-sub1.your-storagebox.de
  User u123456-sub1
  Port 22
  IdentityFile /etc/restic/storagebox_key

Danach kann restic die Storage Box einfach über den Namen storagebox ansprechen:

bash
sudo restic -r sftp:storagebox:restic --password-file /etc/restic/password init

Zusätzlich habe ich in der Hetzner Console automatische Snapshots der Storage Box eingeschaltet. Der Grund: Über SFTP kann restic nicht im „Nur-Anhängen“-Modus arbeiten. Wer den Server übernimmt, könnte also theoretisch auch das Backup löschen. Die Snapshots der Storage Box kann ein Sub-Account aber nicht löschen, damit gibt es noch eine zweite Sicherheitsebene.


Datenbanken: nicht einfach die Dateien kopieren

Das war für mich der lehrreichste Teil. Wenn man die Dateien einer laufenden Datenbank einfach kopiert, kann es sein, dass man sie mitten in einem Schreibvorgang erwischt. Das Backup ist dann im schlimmsten Fall kaputt, und das merkt man erst beim Wiederherstellen.

Deshalb erzeugt mein Backup-Skript vor jedem Lauf konsistente Kopien der Datenbanken in einem eigenen Ordner, und restic sichert dann diese Kopien.

Für SQLite (z. B. Vaultwarden, Uptime Kuma) gibt es dafür den eingebauten .backup-Befehl, der auch bei laufender Anwendung eine saubere Kopie macht:

bash
sqlite3 /srv/appdata/vaultwarden/db.sqlite3 ".backup '/srv/backup-dumps/vaultwarden.sqlite3'"

Für PostgreSQL (z. B. Immich, Paperless) läuft pg_dump direkt im Datenbank-Container:

bash
docker exec paperless-db pg_dump --clean --if-exists --username=paperless paperless \
  | gzip > /srv/backup-dumps/paperless.sql.gz

Die eigentlichen Datenbank-Ordner schließe ich vom Backup aus, die bringen ohne Dump nichts.


Das eigentliche Backup

Was gesichert wird und was nicht, steht in einer Ausschluss-Datei. Rausfliegen zum Beispiel die Thumbnails von Immich (die kann Immich jederzeit neu erzeugen) und die rohen Datenbank-Ordner:

text
# /etc/restic/excludes
/srv/appdata/*/postgres
/srv/appdata/immich/data/thumbs
/srv/appdata/immich/data/encoded-video
*.sqlite3-wal
*.sqlite3-shm

Und dann der eigentliche Befehl:

bash
sudo restic -r sftp:storagebox:restic --password-file /etc/restic/password \
  backup --one-file-system --exclude-caches --exclude-file=/etc/restic/excludes \
  --tag daily /srv/backup-dumps /srv/appdata /srv/data

Der erste Lauf hat bei mir mehrere Stunden gedauert, weil alle Fotos einmal hochgeladen werden mussten. Seitdem dauert ein Lauf nur noch ein paar Minuten.

Alte Snapshots aufräumen

Sonst wächst das Repository ja endlos. restic kann alte Stände nach Regeln löschen. Ich behalte die letzten 7 Tage, 4 Wochen und 12 Monate:

bash
sudo restic -r sftp:storagebox:restic --password-file /etc/restic/password \
  forget --prune --keep-daily 7 --keep-weekly 4 --keep-monthly 12

Prüfen, ob die Daten noch lesbar sind

Einmal pro Woche lässt mein Skript restic das Repository prüfen und dabei zufällig 5 % der Daten tatsächlich herunterladen und entschlüsseln. So fällt es auf, wenn auf dem Speicher etwas kaputtgeht, ohne dass jedes Mal das ganze Backup gelesen werden muss:

bash
sudo restic -r sftp:storagebox:restic --password-file /etc/restic/password \
  check --read-data-subset=5%

Automatisch jede Nacht mit systemd

Das alles steckt in einem Skript, das ein systemd-Timer jede Nacht um 3:30 Uhr startet. Ich mag systemd-Timer lieber als Cronjobs, weil man die Logs direkt mit journalctl sieht und Persistent=true einen verpassten Lauf nachholt, falls der Server um 3:30 Uhr aus war:

ini
# /etc/systemd/system/homelab-backup.timer
[Unit]
Description=Tägliches Homelab-Backup
 
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15min
Persistent=true
 
[Install]
WantedBy=timers.target

Aktivieren und den nächsten Termin anzeigen:

bash
sudo systemctl enable --now homelab-backup.timer
systemctl list-timers homelab-backup.timer

Zwischendurch von Hand starten und zuschauen geht so:

bash
sudo systemctl start --no-block homelab-backup
journalctl -fu homelab-backup

Das Skript sichert nacheinander in beide Ziele. Fällt eins aus, zum Beispiel weil das NAS gerade aus ist, wird das andere trotzdem gesichert und der Fehler gemeldet.


Woher weiß ich, dass es funktioniert?

Ein Backup, von dem man nicht weiß, ob es läuft, ist fast so schlimm wie gar keins. Deshalb gibt es drei Wege, wie ich davon erfahre:

1. Uptime Kuma (Push-Monitor). Am Ende jedes Laufs schickt das Skript ein kurzes „alles gut“ an Uptime Kuma. Bleibt die Meldung länger als einen Tag aus, bekomme ich eine Push-Nachricht aufs Handy:

bash
curl -fsS -m 10 -G "http://127.0.0.1:3001/api/push/${BACKUP_KUMA_PUSH_TOKEN}" \
  --data-urlencode "status=up" --data-urlencode "msg=OK"

2. Prometheus und Grafana. Das Skript schreibt ein paar Werte als Textdatei für den node-exporter, zum Beispiel wann das letzte erfolgreiche Backup war. Daraus gibt es einen Alarm, wenn das letzte Backup älter als 26 Stunden ist:

text
homelab_backup_last_success_timestamp_seconds 1792200614
homelab_backup_last_result 1

3. Ein Dashboard mit Fortschrittsbalken. restic kann seinen Fortschritt mit --json ausgeben. Den lese ich mit einem kleinen Skript mit und zeige ihn in Grafana an: welches Ziel gerade dran ist, wie viel schon geschafft ist und wie lange es noch dauert. Dazu gibt es einen „Backup jetzt“-Knopf, der hinter dem Login liegt. Gebraucht hätte ich das nicht, aber es war ein cooles Mini-Projekt, und ich sehe auf einen Blick, ob alles läuft.


Der wichtigste Schritt: Wiederherstellen üben

Ein Backup ist erst dann ein Backup, wenn man es auch zurückspielen kann. Deshalb habe ich einmal bewusst einen Dienst (Uptime Kuma) komplett gelöscht und aus dem Backup wiederhergestellt.

Zuerst schauen, welche Snapshots es gibt:

bash
sudo restic -r sftp:storagebox:restic --password-file /etc/restic/password snapshots

Dann nur die benötigten Ordner aus dem neuesten Snapshot in ein temporäres Verzeichnis holen:

bash
sudo restic -r sftp:storagebox:restic --password-file /etc/restic/password \
  restore latest --target /tmp/restore \
  --include /srv/appdata/uptime-kuma \
  --include /srv/backup-dumps/uptime-kuma.sqlite3

Danach die Ordner an ihren Platz kopieren, die gesicherte Datenbank-Kopie als kuma.db darüberlegen und den Container wieder starten. Kurz darauf war alles wieder da, inklusive Verlauf.

Für eine Datenbank aus PostgreSQL geht das Einspielen ähnlich, nur eben mit dem Dump:

bash
gunzip -c /srv/backup-dumps/paperless.sql.gz \
  | docker exec -i paperless-db psql -U paperless -d paperless

Ein praktischer Trick zum Stöbern: restic mount hängt alle Snapshots als normales Verzeichnis ein. Dann kann man sich mit ls und cp einzelne Dateien von einem bestimmten Tag herausholen:

bash
sudo mkdir -p /mnt/restic
sudo restic -r /mnt/backup/restic/docker-vm --password-file /etc/restic/password mount /mnt/restic

Ich habe mir vorgenommen, so einen Test einmal im Jahr zu wiederholen, und es steht als wiederkehrende Aufgabe in meiner Roadmap.


Fazit

Das Einrichten hat länger gedauert, als ich gedacht hätte, vor allem die Sache mit den Datenbanken und das Monitoring drumherum. Aber jetzt habe ich zum ersten Mal das Gefühl, dass meine Daten wirklich sicher sind und nicht nur „irgendwo noch eine Kopie liegt“.

Was als Nächstes kommt: Im November ersetzt ein neues NAS mit TrueNAS das alte ReadyNAS. Dann wandert die lokale Kopie dorthin, und ich will mir anschauen, ob ich zusätzlich ZFS-Snapshots nutze.

Falls ihr selbst noch keine Backups für euer Homelab habt: Fangt einfach mit einem Ziel an. Ein restic-Repository auf einer externen Festplatte ist in zehn Minuten eingerichtet und viel besser als nichts.


Zum Weiterlesen

Wer tiefer einsteigen will, findet hier die Quellen, die mir beim Einrichten am meisten geholfen haben: