← Übersicht  ·  Skripte & Dateien  ·  cloud-host · Betrieb

Backup & Restore

Verschlüsselte, automatische Backups deines Hetzner-Servers mit Restic auf eine Hetzner Storage Box – Dateien und Datenbanken, mit Aufbewahrungsregeln, einem Totmann-Schalter zur Überwachung, und – das Wichtigste – einem getesteten Restore: von der einzelnen Datei bis zum kompletten Neuaufbau auf einem frischen Server.

Stand: 30. August 2026 Restic + Storage Box BX11 ~0,13 €/Tag

Platzhalter einsetzen – nur im Browser

Voraussetzung: ein eingerichteter Server – einmaliger Aufbau in Server-Basis, wegwerfen & identisch neu in Snapshot & Restore. Was die Bausteine hier kosten: Kosten & Budget.

Was gesichert werden muss

Nicht alles ist Backup-würdig. Sichere, was du nicht neu erzeugen kannst – der Rest steht in Git und in den Anleitungen.

Die Frage, die in sechs anderen Zeichnungen nebenbei vorkommt – hier einmal vollständig.

Zeichnung aus Diagramme: Hetzner & CRC (36).

Containerneu gestartet Pod neuerstellt Nodeneu gestartet Cluster neuaufgebaut Servergelöscht Image-Schicht liegt in der Registry schreibbare Schicht alles, was der Container schreibt Docker-Volume auf dem Server, außerhalb des Stapels emptyDir gehört dem Pod PersistentVolumeClaim gehört dem Cluster Snapshot des Servers liegt bei Hetzner restic-Repository liegt auf der Storage Box ××××× ×× ×××× ×× hier verschwanden die sieben Datenbanken Die eine Zeile, die man sich merken muss: emptyDir überlebt einen Container-Neustart, aber keinen Pod-Wechsel. Und ein Pod wird nicht nur beim Deployen neu erstellt, sondern auch, wenn der Node ihn verdraengt – etwa unter DiskPressure. Genau so kam es dazu. Drei Zeilen überstehen alles: die beiden untersten und, ganz oben, die Image-Schicht – alle drei liegen außerhalb der Maschine. Was dazwischen steht, hängt an etwas, das man löschen kann. Das Image bringt aber nur den Code zurück, nie die Daten – dafür bleibt allein das Backup.
Fünf Ereignisse, sieben Aufbewahrungsorte – und drei überstehen alles. Der rot umrandete Kasten ist der Fall, der in dieser Sammlung tatsächlich eingetreten ist: emptyDir gegen „Pod neu erstellt“. Kein Fehler im Betrieb, sondern eine bewusst gesetzte Einstellung, deren Reichweite unterschätzt wurde.
WasBackup?warum
Datenbank-Inhalte (Postgres, MySQL, SQLite)ja – als Dumpnicht reproduzierbar, laufende Dateien nie direkt kopieren
Nutzer-Uploads / Volumes (/opt/sites, Nextcloud-Daten, Vaultwarden-data/)jaentsteht zur Laufzeit, weg = weg
compose.yaml, .env, Traefik-Config, /etc-Anpassungenja (klein)schneller Wiederaufbau, .env enthält Secrets
Restic-Repo-Passwort, Storage-Box-Key, API-Tokensja – in den Passwortmanager, nicht ins Repoohne Repo-Passwort ist das Backup wertlos
Docker-Imagesneindocker compose pull holt sie neu (Tags pinnen!)
OS (Betriebssystem) / installierte Paketeneincloud-init / Snapshot baut den Server neu
Logs, Caches, node_modules, /tmpneinBallast, per --exclude raus
ServerDB-Dump + Dateien → restic backup (dedupliziert, komprimiert, verschlüsselt)
nächtlichsystemd-Timer 03:15 → danach Ping an einen Totmann-Schalter
Storage BoxRestic-Repo per SFTP (Port 23). Nur verschlüsselte Blöcke, Hetzner sieht nichts.
wchtl.restic forget --prune (Aufbewahrung) + restic check (Integrität)
optionalrestic copy → zweites Repo (andere Storage Box / Backblaze B2) gegen „Hetzner weg / Ransomware“
3–2–1

Klassische Regel: 3 Kopien der Daten, auf 2 verschiedenen Medien, 1 davon außer Haus. Hier: die Live-Daten am Server, das Restic-Repo auf der Storage Box, und – wenn dir die Daten wirklich wichtig sind – eine Zweitkopie bei einem anderen Anbieter (Abschnitt 10).

1 Storage Box anlegen

Eine Storage Box ist günstiger Speicher mit SSH (Secure Shell)/SFTP/rsync – ideal als Backup-Ziel. Anlegen in der Hetzner-Console (oder im Robot).

  1. console.hetzner.com Storage Storage Boxes Storage Box erstellenBX11 (1 TB, ~3,81 €/Monat), Standort Falkenstein oder Helsinki.
  2. Danach öffnen → Reiter Snapshots aktivieren (die Storage Box kann sich selbst versionieren – günstig, zweite Verteidigungslinie gegen versehentliches restic forget).
  3. Reiter Einstellungen: SSH-Support und externe Erreichbarkeit sind an. Notiere den Hostnamen u123456.your-storagebox.de.
  4. Reiter UnterkontenUnterkonto erstellen: eigener Login u123456-sub1, eigenes Home-Verzeichnis (z. B. /podman-host), Zugriff Lesen + Schreiben, SSH an. So hängt nicht der Hauptzugang am Server.
Server · als root
# eigenen SSH-Key nur fuers Backup (ohne Passphrase, der Timer laeuft unbeaufsichtigt)
ssh-keygen -t ed25519 -N '' -f /root/.ssh/storagebox -C backup@podman-host

# Storage Boxes hoeren SSH auf Port 23. Key hochladen (einmalig, fragt nach dem Sub-Konto-Passwort):
cat /root/.ssh/storagebox.pub | ssh -p23 u123456-sub1@u123456.your-storagebox.de install-ssh-key
Datei · /root/.ssh/config
Host storagebox
  HostName u123456.your-storagebox.de
  User u123456-sub1
  Port 23
  IdentityFile /root/.ssh/storagebox
  IdentitiesOnly yes
Server · Test
ssh storagebox ls        # muss ohne Passwort durchgehen

2 Restic installieren & Repo initialisieren

Ein statisches Binary, keine Abhängigkeiten. Das Repo-Passwort verschlüsselt alles – geht es verloren, sind die Backups nicht wiederherstellbar.

Warum eine laufende Datenbank nie als Datei gesichert wird – und wo der Dump entlangläuft.

Zeichnung aus Diagramme: Hetzner & CRC (06).

Postgres im Container läuft weiter MariaDB im Container --single-transaction SQLite (Vaultwarden) .backup, nicht kopieren Dateien & Volumes /opt, /etc/docker Dump über die Pipe --stdin --stdin-filename landet nie auf der Platte --tag db docker exec – lokaler Unix-Socket, Ausgabe direkt in die Pipe konsistente Kopie liegt im Volume und wird mitgesichert restic-Repository Storage Box, dedupliziert, verschlüsselt --keep-daily 7 · --keep-weekly 5 --keep-monthly 12 · --prune Regeln wirken zusammen, nicht nacheinander SFTP über SSH wer prüft den Waechter? montags: check --read-data-subset=5% täglich: Totmann-Schalter meldet den Lauf 12:00: zweiter Timer prüft das Alter Der dritte Punkt rechts ist der wichtigste: der Totmann-Schalter meldet, wenn ein Backup läuft. Ein Backup, das gar nicht erst startet, schweigt auch beim Melden – genau diese Lücke schließt die Altersprufung.
Datenbanken werden gedumpt, nie als Datei kopiert. Eine laufende Datenbank hat Dateien in einem Zustand, aus dem sich nichts zuverlässig wiederherstellen laesst. Der Dump geht direkt in die Pipe nach restic, also ohne Zwischendatei – kein Platzbedarf und nichts, was jemand liegen laesst.
Server · als root
apt-get install -y restic       # Debian/Ubuntu; danach immer aktuell halten:
restic self-update

# Repo-Passwort erzeugen und sicher ablegen -- UND in den Passwortmanager kopieren!
umask 077
openssl rand -base64 24 > /root/.restic-pass

# Umgebung fuer restic (Repo-Ziel + Passwortdatei)
cat > /root/.restic-env <<'EOF'
export RESTIC_REPOSITORY="sftp:storagebox:/restic"
export RESTIC_PASSWORD_FILE="/root/.restic-pass"
export RESTIC_COMPRESSION="auto"
EOF
chmod 600 /root/.restic-pass /root/.restic-env

# Repo anlegen (einmalig)
source /root/.restic-env
restic init
Das Repo-Passwort ist der Einzelpunkt

Schreib es jetzt in deinen Passwortmanager (Vaultwarden …), zusammen mit dem Hostnamen und dem Repo-Pfad. Ohne diese drei Dinge nützt dir das Backup nichts – auch nicht, wenn der Server noch läuft. Der Restore-Test in Abschnitt 9 prüft genau das mit.

3 Erste Sicherung

Server · als root
source /root/.restic-env

restic backup /opt /etc/docker /etc/ssh/sshd_config \
  --tag files \
  --exclude-caches \
  --exclude '/opt/**/node_modules' \
  --exclude '/opt/**/.git' \
  --exclude '*.log' \
  --exclude '/opt/backups'

restic snapshots        # zeigt den frischen Snapshot mit Host, Zeit, Tags
restic stats            # Groesse im Repo (dedupliziert)

Der \ am Zeilenende ist die Zeilenfortsetzung der Shell – er muss das letzte Zeichen der Zeile sein, ein Leerzeichen dahinter zerlegt den Befehl still in unvollständige Teile. In den PowerShell-Blöcken der anderen Anleitungen steht dafür ein Backtick (`); die beiden lassen sich nicht gegeneinander tauschen, denn in der Shell ist ein Backtick Kommando-Substitution. Restic dedupliziert: die zweite Sicherung überträgt nur die geänderten Blöcke; was --exclude-caches und die Muster genau tun, steht bei Abschnitt 5.

4 Datenbanken richtig

Eine laufende Datenbank hat Dateien in inkonsistentem Zustand. Sichere immer einen Dump – und streame ihn direkt in Restic, ohne Zwischendatei.

Datei · /opt/backup/db-dump.sh
#!/bin/sh
set -eu
. /root/.restic-env

# Postgres (Container-Name anpassen): alle DBs + Rollen
docker exec -i postgres pg_dumpall -U postgres \
  | restic backup --stdin --stdin-filename pg_dumpall.sql --tag db

# MariaDB/MySQL
# docker exec -i mariadb sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' \
#   | restic backup --stdin --stdin-filename mysql-all.sql --tag db

# SQLite (z. B. Vaultwarden) -- konsistente Kopie per .backup, dann normal mitsichern
docker exec vaultwarden sh -c 'sqlite3 /data/db.sqlite3 ".backup /data/db-backup.sqlite3"'
Was die Schalter bedeuten

Drei Datenbanksorten, drei Verfahren – und ein gemeinsamer Grundsatz: eine laufende Datenbank wird nie als Datei kopiert.

KonstruktStellebewirkt
#!/bin/sh Zeile 1 Die Shebang-Zeile bestimmt, womit die Datei ausgeführt wird. Hier bewusst sh und nicht bash: das Skript kommt ohne bash-Erweiterungen aus und läuft damit auch in schlanken Umgebungen. Wirksam wird die Zeile nur, wenn die Datei ausführbar ist (chmod +x).
set -eu Kopf -e bricht beim ersten fehlgeschlagenen Befehl ab, -u beim Zugriff auf eine ungesetzte Variable. Bei einem Backup-Skript ist das keine Kleinigkeit: ohne -e läuft es nach einem Fehler weiter und meldet am Ende Erfolg.
. /root/.restic-env Kopf Der Punkt ist source – die Datei läuft in dieser Shell, ihre Variablen (Repo-Adresse, Passwort, Zugangsschlüssel) gelten danach hier. Als Skript aufgerufen würde sie eine eigene Shell öffnen und die Variablen mit ihr wieder verwerfen. Die Datei gehört auf chmod 600.
docker exec -i postgres pg_dumpall Postgres -i hängt die Eingabe des Containers an. Für diese Richtung braucht man es streng genommen nicht – gepipet wird die Ausgabe von pg_dumpall. Es steht hier aus Gewohnheit, und beim Zurückspielen (docker exec -i postgres psql < dump.sql) ist es dann wirklich nötig. Worauf es ankommt, ist das fehlende -t: ein Terminal würde Steuerzeichen in den Datenstrom einfügen und den Dump unbrauchbar machen. pg_dumpall statt pg_dump nimmt alle Datenbanken samt Rollen mit; bei mehreren Diensten in einer Instanz ist das der Unterschied zwischen „alles da“ und „eine Datenbank vergessen“.
| restic backup --stdin --stdin-filename pg_dumpall.sql Postgres Der Dump geht direkt in die Sicherung, ohne je auf der Platte zu landen – kein Platzbedarf, keine Datei, die jemand liegen lässt. --stdin-filename gibt ihm dabei einen Namen; ohne den hiesse er im Snapshot stdin und wäre beim Zurückholen nicht zuzuordnen. Achtung: ohne -o pipefail (das sh hier nicht kennt) meldet die Pipe Erfolg, sobald restic zufrieden ist – auch wenn pg_dumpall davor abgebrochen ist. Die Grösse des Snapshots im Blick behalten.
--tag db Postgres Markiert den Snapshot. Erst dadurch kann die Aufbewahrungsregel im Backup-Skript (restic forget --tag auto --tag db) gezielt greifen – und restic snapshots --tag db zeigt nur die Datenbank-Sicherungen.
sqlite3 … ".backup /data/db-backup.sqlite3" SQLite SQLite hat keinen externen Dump-Dienst; .backup ist der eingebaute Befehl, der eine konsistente Kopie zieht, während der Dienst weiterläuft. Die Datei einfach zu kopieren ginge schief, sobald gerade eine Transaktion offen ist – das Ergebnis ist dann still beschädigt. Die Kopie liegt im Datenverzeichnis und wird beim Datei-Backup einfach mitgenommen.
MariaDB-Zeilen auskommentiert Mitte Vorlage zum Aktivieren, kein toter Code. Auffallend ist das exec darin: es ersetzt die Shell im Container durch mysqldump, sodass dessen Exit-Code unverfälscht ankommt – ohne das meldet die umgebende Shell immer Erfolg.
Faustregel

Erst dumpen, dann Dateien sichern. Vaultwarden/Nextcloud im laufenden Betrieb zu sichern ist meist ok (SQLite-.backup bzw. Nextcloud im Wartungsmodus), Postgres/MySQL nie als rohe Dateien – nur als Dump.

5 Backup-Skript

Ein Skript, das der Timer aufruft: DB-Dumps, Datei-Backup, Aufbewahrung, Integritätsprüfung (montags), Erfolgsmeldung.

Datei · /opt/backup/run.sh (chmod 700, root)
#!/bin/sh
set -eu
. /root/.restic-env

HC_URL="https://hc-ping.com/DEIN-UUID"   # Totmann-Schalter, Abschnitt 10
curl -fsS -m 10 --retry 3 "$HC_URL/start" || true

# 1) Datenbanken -> Dump in ein temporaeres Verzeichnis
mkdir -p /opt/backup/dumps
docker exec -i postgres pg_dumpall -U postgres > /opt/backup/dumps/pg_dumpall.sql 2>/dev/null || true
docker exec vaultwarden sh -c 'sqlite3 /data/db.sqlite3 ".backup /data/db-backup.sqlite3"' || true

# 2) Dateien + Dumps sichern
restic backup /opt /etc/docker /etc/ssh/sshd_config \
  --tag auto --exclude-caches \
  --exclude '/opt/**/node_modules' --exclude '/opt/**/.git' \
  --exclude '*.log' --exclude '/opt/backup/dumps/*.tmp'

# 3) Aufbewahrung anwenden + alte Daten wirklich loeschen
restic forget --tag auto --tag db \
  --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --prune

# 4) montags: Repo-Integritaet pruefen (5% der Daten wirklich lesen)
[ "$(date +%u)" = "1" ] && restic check --read-data-subset=5%

# 5) Erfolg melden (nur wenn wir bis hier kamen -- set -e bricht sonst ab)
curl -fsS -m 10 --retry 3 "$HC_URL" || true
Was die Schalter bedeuten

Vier restic-Aufrufe und ein paar Kleinigkeiten drumherum, die leicht übersehen werden – und ohne die das Skript still das Falsche täte:

SchalterZeilebewirkt
set -eu Kopf -e bricht beim ersten fehlgeschlagenen Befehl ab, -u beim Zugriff auf eine ungesetzte Variable. Beides zusammen ist der Grund, warum die Erfolgsmeldung ganz unten überhaupt etwas aussagt: sie wird nur erreicht, wenn nichts dazwischen gescheitert ist.
. /root/.restic-env Kopf Der Punkt ist source – die Datei läuft in dieser Shell, ihre Variablen (Repo-Adresse, Passwort, S3-Schlüssel) gelten danach hier. Ein Aufruf als Skript würde eine eigene Shell öffnen und die Variablen mit ihr wieder verwerfen.
|| true Ping-Zeilen, Dumps Hebt set -e für genau diese Zeile auf. Absicht: ein nicht erreichbarer Totmann-Dienst oder ein gerade nicht laufender Container darf das Backup nicht verhindern. Bei restic backup steht es bewusst nicht – dort soll ein Fehler abbrechen.
curl -fsS -m 10 --retry 3 Totmann-Schalter Vier Schalter, die zusammengehören: -f lässt curl bei HTTP-Fehlern auch mit einem Fehlercode enden statt die Fehlerseite auszugeben, -s schaltet die Fortschrittsanzeige ab, -S holt die echten Fehlermeldungen trotz -s zurück, -m 10 deckelt den ganzen Aufruf auf zehn Sekunden. Ohne -m kann ein hängender Dienst den nächtlichen Lauf beliebig lange blockieren.
--stdin --stdin-filename pg_dumpall.sql Abschnitt 4 oben restic sichert den Datenstrom direkt aus der Pipe, ohne dass der Dump je auf der Platte landet. --stdin-filename gibt ihm dabei einen Namen – sonst hiesse er im Snapshot stdin und wäre beim Zurückholen nicht zuzuordnen.
2>/dev/null hinter pg_dumpall Schritt 1 Verwirft nur die Fortschritts- und Hinweisausgabe von docker exec; der Dump selbst geht über die Standardausgabe in die Datei und bleibt unberührt.
--exclude-caches Schritt 2 Überspringt jedes Verzeichnis, das eine CACHEDIR.TAG-Datei enthält – die legen viele Werkzeuge selbst an. Kostet nichts und spart oft mehr als alle handgeschriebenen Ausschlüsse zusammen.
--exclude '/opt/**/node_modules' Schritt 2 ** steht für beliebig viele Verzeichnisebenen, ein einfaches * nur für eine. Die einfachen Anführungszeichen sind Pflicht: ohne sie löst die Shell das Muster selbst auf – gegen die Verzeichnisse, die gerade existieren – und restic bekommt entweder eine feste Liste oder das Muster unverändert zurück. Beides trifft nicht das Gemeinte.
--keep-daily 7 --keep-weekly 5
--keep-monthly 12
Schritt 3 Die Aufbewahrung. Die Regeln wirken zusammen, nicht nacheinander: ein Snapshot bleibt, wenn ihn mindestens eine der drei beansprucht. Ein Sonntags-Snapshot zählt also gleichzeitig als Tages- und als Wochenexemplar. Unterm Strich rund 24 Snapshots über ein Jahr.
--tag auto --tag db Schritt 3 Mehrfaches --tag bei forget ist ein Oder: die Regeln gelten für beide Gruppen. Der Filter ist wichtig – ohne ihn würde forget auch von Hand angelegte Snapshots aus demselben Repo mit aufräumen.
--prune Schritt 3 Der Schritt, der wirklich Platz schafft. forget allein entfernt nur die Verweise; erst prune löscht die dann unbenutzten Datenblöcke. Es ist der langsame Teil des Laufs und der einzige, der das Repo umschreibt – deshalb nie zwei Backups gleichzeitig laufen lassen.
[ "$(date +%u)" = "1" ] && … Schritt 4 %u ist der Wochentag als Zahl, Montag = 1. An allen anderen Tagen ist der Test unwahr – und weil er die letzte Anweisung vor dem Erfolgs-Ping ist, würde set -e das Skript hier an sechs von sieben Tagen beenden. Dass die Erfolgsmeldung trotzdem kommt, liegt allein am || true dahinter. Wer die Zeile verschiebt, muss das mitdenken.
restic check --read-data-subset=5% Schritt 4 check allein prüft nur die Verwaltungsdaten. Erst --read-data-subset lädt echte Datenblöcke herunter und rechnet ihre Prüfsummen nach – das findet stille Datenfehler im Speicherziel. 5 % wöchentlich heißt: nach gut einem Jahr war jeder Block einmal dran, ohne den Datenverkehr in die Höhe zu treiben.

6 Automatisch: systemd-Timer

Zuverlässiger als cron: holt verpasste Läufe nach (Persistent=true) und die Logs stehen im Journal.

Datei · /etc/systemd/system/backup.service
[Unit]
Description=Restic backup
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
Nice=10
IOSchedulingClass=idle
ExecStart=/opt/backup/run.sh
Datei · /etc/systemd/system/backup.timer
[Unit]
Description=Restic backup, taeglich

[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=20m
Persistent=true

[Install]
WantedBy=timers.target
Server · als root
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl start backup.service          # jetzt einmal von Hand
journalctl -u backup.service -f         # zusehen
systemctl list-timers backup.timer      # naechster Lauf
Snapshot-Workflow (cloud.ps1 / snapshot.ps1)

Wenn du den Server abends löschst, läuft der 03:15-Timer nicht. Dann entweder das Backup vor down anstoßen (ssh … systemctl start backup.service im preDown-Hook), oder das Repo vom Laptop aus ziehen, während der Server läuft.

7 Aufbewahrung & Pflege

Regelbedeutet
--keep-daily 7die letzten 7 Tage je ein Snapshot
--keep-weekly 5zusätzlich 5 Wochen-Stände
--keep-monthly 12zusätzlich 12 Monats-Stände
--prunegibt den Platz nicht mehr referenzierter Blöcke frei (langsam – darum im Skript, nicht bei jedem Backup zwingend)
Server · gelegentlich von Hand
restic snapshots                          # was liegt im Repo
restic stats --mode raw-data              # echter Speicherbedarf
restic check                              # Metadaten-Konsistenz (schnell)
restic check --read-data                  # ALLES wirklich lesen (langsam, monatlich sinnvoll)
restic unlock                             # falls ein abgebrochener Lauf ein Lock hinterlassen hat

8 Restore testen – der eigentliche Zweck

Ein Backup, das nie zurückgespielt wurde, ist kein Backup. Alle drei Fälle einmal durchspielen – am besten jetzt, nicht im Notfall.

a) Eine einzelne Datei zurückholen

Server / beliebiger Rechner mit Repo-Zugang
source /root/.restic-env
restic restore latest --target /tmp/restore --include /opt/sites/blog/index.html
restic dump latest /opt/stacks/wordpress/compose.yaml     # direkt auf stdout

b) Im Repo stöbern (wie ein Dateisystem)

Server · braucht fuse
apt-get install -y fuse3
mkdir -p /mnt/restic && restic mount /mnt/restic
# in einem zweiten Terminal: ls /mnt/restic/snapshots/latest/ ...  danach: fusermount3 -u /mnt/restic

c) Datenbank zurückspielen

Server
# Postgres komplett aus dem Dump im Repo
restic dump latest /pg_dumpall.sql | docker exec -i postgres psql -U postgres

# Vaultwarden: die db-backup.sqlite3 aus dem Datei-Backup zuruecklegen und Container neu starten

d) Kompletter Neuaufbau (Disaster-Probe)

  1. Neuen Server aus dem Basis-Image/Snapshot erzeugen (cloud.ps1 up bzw. Server-Snapshot & Restore-Anleitung).
  2. Restic installieren, /root/.ssh/config + Key + /root/.restic-env + Repo-Passwort aus dem Passwortmanager wiederherstellen.
  3. restic restore latest --target / – legt /opt, Configs usw. zurück. Danach DB-Dump einspielen (c).
  4. cd /opt/stacks/<stack> && docker compose pull && docker compose up -d für jeden Stack. DNS (Domain Name System) ggf. auf die neue IP (Internet Protocol) ziehen (cf-dns.ps1).
  5. Aufräumen: den Test-Server wieder löschen.
Bestanden, wenn

… die Anwendung auf dem frischen Server läuft und die Daten von gestern zeigt – nur mit dem, was im Passwortmanager und im Repo steht. Miss die Zeit: das ist deine reale Recovery Time.

9 Backups überwachen

Das gefährlichste Backup ist das, von dem du glaubst, dass es läuft. Du brauchst eine Meldung, wenn es ausbleibt.

  • Totmann-Schalter (im Skript schon eingebaut): ein Dienst wie healthchecks.io (Free) oder selbstgehostet erwartet täglich einen Ping. Bleibt er aus → E-Mail/Telegram/ntfy. Kommt zuerst /start aber nie das Ende → ebenfalls Alarm.
  • Snapshot-Alter prüfen (falls kein externer Dienst): ein zweiter Timer, der restic snapshots --json --latest 1 liest und meckert, wenn der neueste Snapshot älter als 36 h ist.
  • Nach jedem größeren Deploy einmal von Hand systemctl start backup.service und in restic snapshots nachsehen.
  • Storage-Box-Füllstand: Console zeigt die Belegung. Läuft sie voll, schlägt restic backup fehl (und der Totmann-Schalter meldet es).
Datei · /opt/backup/check-age.sh (optionaler zweiter Timer, z. B. 12:00)
#!/bin/sh
set -eu
. /root/.restic-env
LAST=$(restic snapshots --json --latest 1 | grep -o '"time":"[^"]*"' | head -1 | cut -d'"' -f4)
AGE=$(( ( $(date +%s) - $(date -d "$LAST" +%s) ) / 3600 ))
[ "$AGE" -gt 36 ] && curl -fsS -m 10 -d "Backup ist $AGE h alt!" ntfy.sh/dein-topic
Was die Zeilen bedeuten

Der Totmann-Schalter meldet, wenn ein Backup läuft. Dieses Skript meldet, wenn seit zu langer Zeit keines mehr angekommen ist – das ist nicht dasselbe, denn ein Backup, das gar nicht erst startet, schweigt auch beim Melden.

KonstruktStellebewirkt
restic snapshots --json --latest 1 LAST --latest 1 holt nur den neuesten Snapshot, --json liefert ihn maschinenlesbar. Ohne beides käme die ganze Liste in Tabellenform – und die lässt sich nicht zuverlässig zerlegen, sobald sich eine Spaltenbreite ändert.
grep -o '"time":"[^"]*"' | head -1 | cut -d'"' -f4 LAST Drei Schritte, um aus dem JSON einen nackten Zeitstempel zu machen: -o gibt nur den Treffer aus statt der ganzen Zeile, head -1 nimmt den ersten (es gibt mehrere Zeitfelder), cut -d'"' -f4 schneidet zwischen dem dritten und vierten Anführungszeichen heraus. Anfällig – ändert restic die Feldreihenfolge, bricht es. Ist jq vorhanden, ist jq -r '.[0].time' die haltbarere Fassung.
$(( … )) AGE Ganzzahl-Arithmetik der Shell. Innerhalb der doppelten Klammern braucht es keine Anführungszeichen und kein expr. Geteilt wird ohne Rest – aus 35,9 Stunden werden 35.
date +%s und date -d "$LAST" +%s AGE %s ist die Sekundenzahl seit 1970 – die einzige Form, in der sich zwei Zeitpunkte gefahrlos voneinander abziehen lassen, ohne über Zeitzonen und Sommerzeit nachzudenken. -d lässt date einen vorgegebenen Zeitpunkt umrechnen statt der aktuellen Zeit; das ist eine GNU-Erweiterung und fehlt in BusyBox- oder Alpine-Umgebungen.
-gt 36 Schwelle 36 Stunden bei einem täglichen Backup – also anderthalb Zyklen. Bewusst nicht 24: ein einzelner Lauf, der sich um zwei Stunden verzögert (langsame Verbindung, grösseres Datenvolumen), soll keinen Fehlalarm auslösen. Zu groß gewählt bemerkt man einen Ausfall dagegen erst Tage später.
[ … ] && curl … Meldung Kurzform für ein if: der zweite Teil läuft nur, wenn der Test wahr ist. Nebenwirkung beachten: ist alles in Ordnung, ist der Test unwahr – und weil er die letzte Anweisung ist, endet das Skript mit einem Fehlercode. Unter set -e ist das hier folgenlos, weil nichts mehr kommt; im Journal des Timers taucht der Lauf aber als fehlgeschlagen auf. Wer das störend findet, hängt || true an oder schreibt es als if aus.
-d "Backup ist $AGE h alt!" Meldung -d schickt die Daten per POST – bei ntfy.sh ist der Rumpf der Text der Benachrichtigung. Die doppelten Anführungszeichen sind hier richtig: nur so wird $AGE eingesetzt.

+ Zweite Kopie außer Haus

Schützt gegen: Hetzner-Ausfall/Account-Sperre, und – wenn die Zweitkopie der Server nicht selbst schreiben kann – gegen Ransomware/versehentliches forget.

Server oder Laptop · restic copy
# Zweitrepo z. B. Backblaze B2 (erste 10 GB gratis, dann ~6 $/TB/Monat)
export RESTIC_REPOSITORY2="s3:s3.eu-central-003.backblazeb2.com/mein-bucket/restic"
export RESTIC_PASSWORD_FILE2="/root/.restic-pass2"
export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...

restic -r "$RESTIC_REPOSITORY2" init          # einmalig
restic copy --from-repo "$RESTIC_REPOSITORY" -r "$RESTIC_REPOSITORY2"
  • Zweite Storage Box in anderem Standort ist die einfachste Variante – aber gleiches Konto, gleicher Anbieter.
  • Pull statt Push: läuft restic copy vom Laptop (oder einem Mini-Server in anderem Hetzner-Projekt) und kennt der Server die Zweitrepo-Zugangsdaten nicht, kann ein kompromittierter Server die Zweitkopie nicht löschen.
  • Storage-Box-eigene Snapshots (Abschnitt 1) sind ein billiger dritter Schutz gegen versehentliches Löschen im Erstrepo.

+ Vom Laptop ziehen statt schieben

Passt zum cloud.ps1-Rhythmus: solange der Server läuft, sichert der Laptop per SSH.

Laptop · PowerShell (restic via winget install restic.restic)
$env:RESTIC_REPOSITORY = "sftp:storagebox:/restic"
$env:RESTIC_PASSWORD_FILE = "$env:USERPROFILE\.restic-pass"

# DB-Dump ueber SSH direkt in restic
ssh podman-host "docker exec -i postgres pg_dumpall -U postgres" `
  | restic backup --stdin --stdin-filename pg_dumpall.sql --tag db --host podman-host

# Dateien: erst per rsync/scp holen, dann sichern -- oder sftp-Backend direkt auf den Server

Nachteil: läuft nur, wenn du und der Server gleichzeitig online sind. Der Server-Timer ist verlässlicher, wenn der Server ohnehin durchläuft.

+ Restic vs Borg vs Snapshot vs Hetzner-Backups

VerfahrensichertWiederherstellungBemerkung
Restic (diese Anleitung)Dateien + gestreamte Dumpseinzelne Datei … allesdedupliziert, verschlüsselt, viele Ziele (SFTP/S3/…). Selbst pflegen.
BorgBackupwie Resticwie ResticÄhnlich. Storage Box hat nativen Borg-Modus. Kein S3.
Server-Snapshot (server-snapshot-restore)ganze Platte, Zustand Xganzer Serverschnelles Wiederherstellen des Setups – aber ein Stand, kein Verlauf, keine Einzeldatei.
Hetzner Auto-Backups (+20 %)ganze Platte, 7 Tageganzer Serverbequem, aber nur 7 Tage, an den Server gebunden, nicht außer Haus.
nacktes rsyncDateienDateienkein Verlauf, kein Dedup, keine Verschlüsselung – nur als Ergänzung.

Zusammen ergibt Sinn: Snapshot für „Server-Setup zurück in 10 Minuten“ + Restic für „die Daten von jedem der letzten 30 Tage, einzeln oder ganz, auch wenn Hetzner weg ist“.

+ Kosten

Preise sind Richtwerte. Hetzner hat 2026 zweimal erhöht (zuletzt 15. Juni: CX/CAX +30–40 %, CPX/CCX über 100 %). Aktuelle Server-Preise, alle Nebenposten, ein Rechner und Beispielrechnungen stehen zentral in Kosten & Budget.

Posten€/Tag€/Monat
Storage Box BX11 (1 TB, SSH/SFTP)~0,13~3,81
Storage-Box-Snapshots (auf der Box)0im Preis
Restic00 (Open Source)
healthchecks.io / ntfy.sh (Überwachung)00 (Free-Tier)
Zweitkopie Backblaze B2 (optional, ~20 GB)~0,004~0,12
Realistisch~0,13~3,8–4

Die Storage Box wird monatlich abgerechnet (kein Stunden-Modell wie die Server). BX21 (5 TB, ~10,90 €) erst, wenn 1 TB (Terabyte) wirklich eng wird – Restic-Repos sind durch Dedup meist klein.

⌂ Cockpit