← Ü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.

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 ausser 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.

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 prueft nur die Verwaltungsdaten. Erst --read-data-subset lädt echte Datenblöcke herunter und rechnet ihre Pruefsummen nach – das findet stille Datenfehler im Speicherziel. 5 % wöchentlich heisst: 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 gross 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 ausser 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 ausser 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.