Lange lief mein Backup host-zentriert: Der Host hat seine Incus-Instanzen von außen gesichert. Skript, Repository und Secrets lagen beim Host, die Instanzen selbst hatten keine Ahnung von ihrem Backup. Das klappte – solange die Instanzen dort blieben.
Ich verschiebe meine Instanzen aber ständig zwischen Hosts hin und her, und genau da wird host-zentrisches Backup zur Belastung. Deshalb habe ich umgestellt: Jede Instanz sichert sich jetzt selbst – gegen eine zentrale Restic-Instanz, die über Netbird von jedem Standort aus erreichbar ist.
Das alte Modell: Der Host als Backup-Zentrale
Im alten Setup war der Host die Backup-Zentrale für alle seine Instanzen – so wie im Praxis-Artikel beschrieben:
Host
├── /backup/restic-backup.sh Backup-Skript für alle Instanzen
├── /backup/.secret.b2 Secrets für die Ziel-Repositories
├── /backup/exclude.txt Ausschlussliste
└── restic Repository z.B. bei B2
Ein Ort, ein Skript, eine Retention-Policy. Klingt zunächst nach wenig Verwaltung – die Nachteile zeigen sich aber spätestens beim Umzug einer Instanz:
- Das Backup-Wissen liegt im Host, nicht in der Instanz. Die Instanz weiß nichts von ihrem Backup. Wird sie auf einen anderen Host kopiert, bleibt die Konfiguration auf dem alten Host zurück, und das Backup läuft irgendwann ins Leere.
- Jeder Host bedeutet ein neues Setup. Bei mehreren Hosts liegt das Backup-Wissen mehrfach in der Gegend herum und muss überall gepflegt werden.
- Die Zuordnung ist Konvention. Welcher Snapshot zu welcher Instanz gehört, ergibt sich nur aus Hostnamen und Konventionen – nicht aus der Struktur.
Das neue Modell: Jede Instanz trägt ihr Backup selbst
Statt den Host zur Zentrale zu machen, wandert das Backup-Wissen in die Instanz: Backup-Skript, Secrets, Retention-Policy. Als Ziel dient eine zentrale Restic-Instanz, die über Netbird eine stabile Adresse behält:
Netbird-Mesh (WireGuard)
┌──────────────┐ sftp:restic:... ┌──────────────────────┐
│ Instanz A │────────────────────>│ │
│ Instanz B │────────────────────>│ Incus-Instanz │
│ Instanz C │────────────────────>│ „restic" (zentral) │
└──────────────┘ └──────────────────────┘
Einer Instanz ist es damit egal, auf welchem Host sie gerade läuft: Sie startet, der Netbird-Client baut das Mesh auf, und sie erreicht ihr Repository unter derselben Adresse wie vorher. Kein Host-spezifisches Setup, kein Umkonfigurieren beim Umzug.
| Altes Modell | Neues Modell | |
|---|---|---|
| Backup-Wissen liegt | beim Host | in der Instanz |
| Instanz zwischen Hosts umziehen | Konfiguration bleibt zurück | alles zieht mit |
| Neuer Host | komplettes neues Setup | nichts zu tun |
| Repository-Zuordnung | pro Host, per Konvention | pro Instanz |
| Ziel erreichbar | nur vom Host aus | von überall (Netbird) |
Warum Netbird?
Die zentrale Instanz braucht eine Adresse, die von jedem Host und jedem Netz aus funktioniert – egal ob daheim im LAN oder bei einem VPS. Genau dafür nutze ich mein [selbst gehostetes Netbird](/artikel/netbird-vergleich-tailscale.html): ein Mesh-VPN auf WireGuard-Basis, in dem sich Geräte gegenseitig über ihren Namen erreichen. Für mich heißt das: Das Backup-Ziel ist einfach restic – ein Name, den jede Instanz im Mesh kennt, unabhängig von Host-Netzwerken und Portforwardings.
Die zentrale Restic-Instanz aufsetzen
Auf der Ziel-Instanz läuft kein eigenes Restic-Programm. Restic ist clientseitig – die Instanz braucht nur SSH/SFTP und Speicherplatz. Ich habe sie als schlanke Incus-Instanz angelegt:
incus launch images:debian/13 restic
incus exec restic -- apt install openssh-server -y
# backup existiert als user im debian 13 image
incus exec restic -- userdel backup
incus exec restic -- useradd -m -d /srv/restic/backup backup
incus exec restic -- mkdir -p /srv/restic/backup/repos /srv/restic/backup/clients
incus exec restic -- chown -R backup:backup /srv/restic/backup
Der Benutzer backup bekommt pro Instanz ein eigenes Verzeichnis als Repository sowie ein Verzeichnis clients/ mit allem, was die Instanz zum Sichern und Wiederherstellen braucht. Passwort-Login ist aus.
# /srv/restic/backup/clients/meine-instanz/
├── backup.env Zugangsdaten (Repository, Passwort)
├── id_restic privater Schlüssel der Instanz
└── restore.sh erzeugt die Instanz neu und stellt wieder her
# /etc/ssh/sshd_config.d/restic.conf
PasswordAuthentication no
Wer es strenger mag, sperrt jeden Client per ChrootDirectory in sein eigenes Repo – dann kann eine kompromittierte Instanz nur ihr eigenes Repository erreichen.
In der Instanz dann noch netbird einrichten:
curl -fsSL https://pkgs.netbird.io/install.sh | sh
netbird up --management-url https://netbird.frischux.de --setup-key <setup key von netbird>
Backup auf einer Instanz einrichten
Das Setup auf jeder Instanz sieht dann so aus. Restic installieren:
# Debian/Ubuntu liefern ein Paket (etwas älter):
apt install restic
# oder das aktuelle Binary von den GitHub-Releases in /usr/local/bin legen
SSH-Schlüssel erzeugen, den öffentlichen Teil auf der Restic-Instanz hinterlegen und den privaten zusätzlich zentral ablegen – sonst kann die Restore-Automatik eine verlorene Instanz nicht mehr rekonstruieren:
ssh-keygen -t ed25519
cat /root/.ssh/id_ed25519.pub # → authorized_keys von backup
scp /root/.ssh/id_ed25519 backup@restic:/srv/restic/backup/clients/meine-instanz/
Umgebung und Repository anlegen:
Die Zugangsdaten liegen nicht in der Instanz selbst, sondern zentral auf der Restic-Instanz – eine backup.env pro Instanz. Denn im Restore-Fall nützt es nichts, wenn die Zugangsdaten erst im Backup liegen würden. Jede Instanz holt sich ihre Datei vor dem Backup per scp:
# /srv/restic/backup/clients/meine-instanz/backup.env (auf der Restic-Instanz)
export RESTIC_REPOSITORY=sftp:backup@restic:repos/meine-instanz
export RESTIC_PASSWORD=<passwort>
restic init
Und das Backup-Skript – es lädt zuerst die zentrale Config herunter:
#!/bin/sh
set -e
scp backup@restic:/srv/restic/backup/clients/meine-instanz/backup.env /backup/env
. /backup/env
restic backup \
--exclude-caches \
--one-file-system \
--files-from /backup/toBackup.files \
--exclude-file /backup/exclude.txt
restic forget --keep-daily 6 --keep-weekly 3 --keep-monthly 18 --prune
restic check --read-data-subset=5%
curl -d "backup ok: $(hostname)" https://ntfy.frischux.de/backup
Dateilisten und Retention kennst du schon aus dem Praxis-Artikel – nur das Ziel hat sich von einem Cloud-Backend auf sftp:restic:... geändert. Gestartet wird das Skript per Cron. Auf meinen Alpine-Instanzen gibt es keinen systemd-Timer, Cron ist aber überall gleich – egal ob busybox-Cron auf Alpine oder Cronie auf Debian:
# /etc/crontab (Debian) bzw. /etc/crontabs/root (Alpine)
0 3 * * * root /usr/local/sbin/restic-backup.sh
Der eigentliche Gewinn: Instanzen wandern, Backups nicht
Die neue Struktur zahlt sich bei jedem Umzug aus:
host-a$ incus export meine-instanz /tmp/meine-instanz.tar.gz
host-b$ incus import /tmp/meine-instanz.tar.gz
host-b$ incus start meine-instanz
Die Instanz startet auf dem neuen Host, der Netbird-Client baut die Verbindung auf, und beim nächsten Cron-Lauf geht das Backup gegen dieselbe Adresse restic wie vorher. Das Repository liegt zentral unter /repos/meine-instanz – die Snapshot-Historie bleibt vollständig erhalten, egal auf welchem Host die Instanz gerade liegt. Genau das macht meine tägliche Arbeit mit Lab-Instanzen einfacher: kopieren, starten, weiterarbeiten, ohne sich über Backups Gedanken zu machen.
Wiederherstellen – automatisiert per restore.sh
Zum Wiederherstellen brauche ich keine Host-Konfiguration und keine kopierten Zugangsdaten von der verlorenen Instanz. Alles Nötige liegt zentral: Neben der backup.env hinterlegt die Restic-Instanz pro Instanz ein restore.sh, das den kompletten Restore macht – Instanz erzeugen, Restore-Werkzeuge installieren (je nach Host- bzw. Basis-Image), Zugang aufbauen, Daten zurückspielen. Ein generisches Beispiel:
# /srv/restic/backup/clients/meine-instanz/restore.sh
#!/bin/sh
set -e
INSTANCE="meine-instanz"
IMAGE="images:debian/13" # pro Instanz hinterlegt
VM="" # leer für Container oder "--vm"
SERVER="restic"
REMOTE="/srv/restic/backup/clients/$INSTANCE"
# 1. Zugang und Schlüssel vom Server holen
scp "backup@$SERVER:$REMOTE/backup.env" /tmp/backup.env
scp "backup@$SERVER:$REMOTE/id_ed25519" /tmp/id_ed25519
# 2. Instanz erzeugen
incus launch "$IMAGE" "$INSTANCE" $VM
while ! incus exec "$INSTANCE" -- true 2>/dev/null; do sleep 1; done
# 3. Restore-Werkzeuge installieren – je nach Basis-Image
incus exec "$INSTANCE" -- sh -c '
if command -v apk >/dev/null 2>&1; then
apk add --no-cache restic openssh-client curl
else
apt-get update && apt-get install -y restic openssh-client curl
fi
mkdir -p /root/.ssh /backup
'
# 4. Config und Schlüssel in die Instanz legen
incus file push /tmp/backup.env "$INSTANCE/backup/env"
incus file push /tmp/id_ed25519 "$INSTANCE/root/.ssh/id_ed25519"
incus exec "$INSTANCE" -- chmod 600 /root/.ssh/id_ed25519
rm -f /tmp/backup.env /tmp/id_ed25519
# 5. Backups zurückspielen
incus exec "$INSTANCE" -- sh -c "
. /backup/env
restic restore latest --target /
"
Auf dem Ziel-Host reichen dann zwei Befehle – der Host braucht lediglich einen SSH-Schlüssel, der bei backup auf der Restic-Instanz hinterlegt ist:
host-a$ scp backup@restic:/srv/restic/backup/clients/meine-instanz/restore.sh .
host-a$ ./restore.sh
EDIT: Dieses spezifische Script funktioniert bei mir nur, weil die restic Instanz auf dem Host läuft, auf dem ich das restore starte. Was hier fehlt, ist die Einbindung ins netbird Netz, dazu müßte man aber auch die Clientdaten (secret key, etc) im restic host mitsichern, ich denke das würde diesen Artikel ausufern lassen, deswegen überlasse ich das dem geneigten Leser als Übungsaufgabe.
Was das bringt:
- Ein Ort für das ganze Restore-Wissen. Image, Instanz-Typ und benötigte Werkzeuge stehen direkt neben den Zugangsdaten – die Frage "was war das für eine Instanz?" beantwortet sich von selbst.
- Systemübergreifend. Das Skript erkennt am Installationspfad, ob es
aptoderapknutzen muss – egal ob Debian-, Ubuntu- oder Alpine-Basis. - Zentral versioniert. Ändert sich die Restore-Prozedur, passe ich eine Datei auf dem Server an – nicht Dutzende Notizen und Halbwissen.
Die verschiedenen Restore-Wege von Restic selbst (Praxis-Artikel) gelten unverändert – restore.sh automatisiert nur das Drumherum. Irgendwann stelle ich dann vielleicht auch mal sauber auf ansible um.
Fazit
Pro Instanz ist einmalig ein bisschen Arbeit nötig: Restic-Binary, SSH-Schlüssel, das zentrale backup.env, das restore.sh, Cron-Job. Danach ist jede Instanz eine autarke Einheit, die ihr Backup überallhin mitnimmt – und im Ernstfall per ./restore.sh wieder aufgebaut werden kann. Dafür spare ich mir die host-zentrische Verwaltung komplett – und der Umzugs-Fall, für den ich das Ganze gebaut habe, ist jetzt ein simples incus export und incus import.
Der Preis: Die zentrale Restic-Instanz bündelt alle Secrets und Repositories – der Zugang zu ihr will entsprechend abgesichert sein. Und auch sie selbst gehört gesichert (aktuell per incus export auf dem Host, auf dem die restic Instanz läuft, das ist dann die berühmte Ausnahme von der Regel): Sie enthält schließlich alle Repositories und Configs, auf die sich der ganze Restore verlässt. Dafür ist das Backup-Problem für meine produktiven INstanzen genau an der Stelle gelöst, wo es entsteht – in der Instanz selbst.
