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.
| Was | Backup? | warum |
|---|---|---|
| Datenbank-Inhalte (Postgres, MySQL, SQLite) | ja – als Dump | nicht reproduzierbar, laufende Dateien nie direkt kopieren |
Nutzer-Uploads / Volumes (/opt/sites, Nextcloud-Daten, Vaultwarden-data/) | ja | entsteht zur Laufzeit, weg = weg |
compose.yaml, .env, Traefik-Config, /etc-Anpassungen | ja (klein) | schneller Wiederaufbau, .env enthält Secrets |
| Restic-Repo-Passwort, Storage-Box-Key, API-Tokens | ja – in den Passwortmanager, nicht ins Repo | ohne Repo-Passwort ist das Backup wertlos |
| Docker-Images | nein | docker compose pull holt sie neu (Tags pinnen!) |
| OS (Betriebssystem) / installierte Pakete | nein | cloud-init / Snapshot baut den Server neu |
Logs, Caches, node_modules, /tmp | nein | Ballast, per --exclude raus |
restic backup (dedupliziert, komprimiert, verschlüsselt)restic forget --prune (Aufbewahrung) + restic check (Integrität)restic copy → zweites Repo (andere Storage Box / Backblaze B2) gegen „Hetzner weg / Ransomware“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).
-
→
BX11(1 TB, ~3,81 €/Monat), Standort Falkenstein oder Helsinki. - Danach öffnen → Reiter Snapshots aktivieren (die Storage Box kann
sich selbst versionieren – günstig, zweite Verteidigungslinie gegen versehentliches
restic forget). - Reiter Einstellungen: SSH-Support und externe Erreichbarkeit
sind an. Notiere den Hostnamen
u123456.your-storagebox.de. - Reiter Unterkonten → Unterkonto 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.
# 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
Host storagebox
HostName u123456.your-storagebox.de
User u123456-sub1
Port 23
IdentityFile /root/.ssh/storagebox
IdentitiesOnly yes
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.
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
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
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.
#!/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"'
Drei Datenbanksorten, drei Verfahren – und ein gemeinsamer Grundsatz: eine laufende Datenbank wird nie als Datei kopiert.
| Konstrukt | Stelle | bewirkt |
|---|---|---|
#!/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. |
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.
#!/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
Vier restic-Aufrufe und ein paar Kleinigkeiten drumherum, die leicht
übersehen werden – und ohne die das Skript still das Falsche täte:
| Schalter | Zeile | bewirkt |
|---|---|---|
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.
[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
[Unit]
Description=Restic backup, taeglich
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=20m
Persistent=true
[Install]
WantedBy=timers.target
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
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
| Regel | bedeutet |
|---|---|
--keep-daily 7 | die letzten 7 Tage je ein Snapshot |
--keep-weekly 5 | zusätzlich 5 Wochen-Stände |
--keep-monthly 12 | zusätzlich 12 Monats-Stände |
--prune | gibt den Platz nicht mehr referenzierter Blöcke frei (langsam – darum im Skript, nicht bei jedem Backup zwingend) |
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
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)
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
# 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)
- Neuen Server aus dem Basis-Image/Snapshot erzeugen (
cloud.ps1 upbzw. Server-Snapshot & Restore-Anleitung). - Restic installieren,
/root/.ssh/config+ Key +/root/.restic-env+ Repo-Passwort aus dem Passwortmanager wiederherstellen. restic restore latest --target /– legt/opt, Configs usw. zurück. Danach DB-Dump einspielen (c).cd /opt/stacks/<stack> && docker compose pull && docker compose up -dfür jeden Stack. DNS (Domain Name System) ggf. auf die neue IP (Internet Protocol) ziehen (cf-dns.ps1).- Aufräumen: den Test-Server wieder löschen.
… 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
/startaber nie das Ende → ebenfalls Alarm. - Snapshot-Alter prüfen (falls kein externer Dienst): ein zweiter Timer,
der
restic snapshots --json --latest 1liest und meckert, wenn der neueste Snapshot älter als 36 h ist. - Nach jedem größeren Deploy einmal von Hand
systemctl start backup.serviceund inrestic snapshotsnachsehen. - Storage-Box-Füllstand: Console zeigt die Belegung. Läuft sie voll,
schlägt
restic backupfehl (und der Totmann-Schalter meldet es).
#!/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
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.
| Konstrukt | Stelle | bewirkt |
|---|---|---|
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.
# 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 copyvom 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.
$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
| Verfahren | sichert | Wiederherstellung | Bemerkung |
|---|---|---|---|
| Restic (diese Anleitung) | Dateien + gestreamte Dumps | einzelne Datei … alles | dedupliziert, verschlüsselt, viele Ziele (SFTP/S3/…). Selbst pflegen. |
| BorgBackup | wie Restic | wie Restic | Ähnlich. Storage Box hat nativen Borg-Modus. Kein S3. |
| Server-Snapshot (server-snapshot-restore) | ganze Platte, Zustand X | ganzer Server | schnelles Wiederherstellen des Setups – aber ein Stand, kein Verlauf, keine Einzeldatei. |
| Hetzner Auto-Backups (+20 %) | ganze Platte, 7 Tage | ganzer Server | bequem, aber nur 7 Tage, an den Server gebunden, nicht außer Haus. |
nacktes rsync | Dateien | Dateien | kein 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) | 0 | im Preis |
| Restic | 0 | 0 (Open Source) |
| healthchecks.io / ntfy.sh (Überwachung) | 0 | 0 (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.