Robuste, inkrementelle Backup-Skripte, die auch bei vollem Speicher kontrolliert scheitern
Ein Backup-Skript, das im Normalfall funktioniert, aber bei vollem Speicherplatz eine unvollständige Archivdatei zurücklässt, die niemand als defekt erkennt, ist schlimmer als gar kein Backup, weil es falsche Sicherheit vorgaukelt. Robuste tar- und gzip-Automatisierung bedeutet, Fehlerfälle genauso sorgfältig zu behandeln wie den Erfolgsfall.
Inhaltsverzeichnis
- 1. Das Grundgerüst eines robusten Backup-Skripts
- 2. Inkrementelle Backups mit --listed-incremental
- 3. Volle und inkrementelle Backups sinnvoll kombinieren
- 4. Fehlerbehandlung bei vollem Speicher: früh erkennen statt spät scheitern
- 5. Kompressions-Level vs. Laufzeit: den richtigen Tradeoff wählen
- 6. Kompression parallelisieren, um den Tradeoff zu entschärfen
- 7. Backups regelmäßig testweise wiederherstellen
- 8. Aufbewahrungsfristen automatisiert durchsetzen
- 9. Backup-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Grundgerüst eines robusten Backup-Skripts
Ein Backup-Skript unterscheidet sich von den meisten anderen Automatisierungsskripten dadurch, dass ein unbemerkter Fehler oft erst Monate später sichtbar wird, nämlich genau dann, wenn das Backup tatsächlich gebraucht wird. Deshalb gehört an den Anfang jedes Backup-Skripts set -euo pipefail, damit ein fehlgeschlagener Befehl das Skript sofort abbricht, statt mit einer stillschweigend unvollständigen Archivdatei weiterzumachen, die später wie ein gültiges Backup aussieht.
Ebenso wichtig ist ein aussagekräftiger Exit-Code und eine strukturierte Log-Ausgabe, damit ein übergeordnetes Monitoring-System sofort erkennt, ob ein Backup-Lauf erfolgreich war, ohne den kompletten Log-Text interpretieren zu müssen. Ein Backup-Skript, das nur bei einem Terminal-Fehler auffällt, aber in einem unbeaufsichtigten Cron-Job stillschweigend fehlschlägt, verfehlt seinen eigentlichen Zweck.
#!/usr/bin/env bash
set -euo pipefail
readonly BACKUP_SRC="/var/www/app"
readonly BACKUP_DST="/backups/app-$(date +%Y%m%d-%H%M%S).tar.gz"
readonly LOG_FILE="/var/log/backup-app.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"
}
trap 'log "FAILED: backup aborted (exit code $?)"' ERR
log "Starting backup: $BACKUP_SRC -> $BACKUP_DST"
tar -czf "$BACKUP_DST" -C "$(dirname "$BACKUP_SRC")" "$(basename "$BACKUP_SRC")"
log "Backup completed successfully: $(du -h "$BACKUP_DST" | cut -f1)"
2. Inkrementelle Backups mit --listed-incremental
Für große Datenbestände ist ein tägliches Vollbackup oft weder nötig noch praktikabel, weil sich zwischen zwei Läufen meist nur ein kleiner Bruchteil der Dateien tatsächlich ändert. tar unterstützt genau für diesen Fall die Option --listed-incremental, die in einer separaten Snapshot-Datei den Zustand aller Dateien beim letzten Lauf festhält und beim nächsten Aufruf nur noch die seither veränderten oder neuen Dateien in das Archiv aufnimmt.
Der entscheidende Unterschied zu einem einfachen Vollbackup liegt in dieser Snapshot-Datei: Sie muss über alle Läufe hinweg erhalten bleiben und darf niemals versehentlich mit dem Archiv selbst gelöscht werden, sonst verliert tar beim nächsten Lauf die Information darüber, was bereits gesichert wurde, und fällt unbemerkt auf ein Vollbackup zurück. Ein robustes Skript legt die Snapshot-Datei deshalb an einem dauerhaften, vom eigentlichen Archivziel getrennten Ort ab.
#!/usr/bin/env bash
set -euo pipefail
readonly SRC="/var/www/app"
readonly SNAPSHOT="/backups/state/app.snapshot"
readonly ARCHIVE="/backups/app-incr-$(date +%Y%m%d).tar.gz"
mkdir -p "$(dirname "$SNAPSHOT")"
# First run: SNAPSHOT does not exist yet -> full backup, snapshot is created
# Subsequent runs: only files changed since the last run are included
tar --listed-incremental="$SNAPSHOT" -czf "$ARCHIVE" -C "$(dirname "$SRC")" "$(basename "$SRC")"
echo "Incremental backup written: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))"
3. Volle und inkrementelle Backups sinnvoll kombinieren
Reine inkrementelle Backups über viele Monate hinweg machen die Wiederherstellung mühsam, weil jedes einzelne Inkrement in der richtigen Reihenfolge eingespielt werden muss, um den aktuellen Stand zu erhalten. Bewährt hat sich deshalb ein rotierendes Schema: einmal pro Woche ein Vollbackup mit einer frischen Snapshot-Datei, an den übrigen Tagen jeweils ein Inkrement gegenüber dem letzten Vollbackup.
Diese Rotation lässt sich direkt im Skript über den Wochentag steuern, indem die Snapshot-Datei am Tag des Vollbackups gezielt gelöscht wird, bevor tar erneut aufgerufen wird. Damit erzwingt das Skript an diesem Tag automatisch ein neues Vollbackup, ohne dass eine separate Codepfad-Verzweigung für Voll- und Inkrementbackup nötig wird, tar erkennt die fehlende Snapshot-Datei selbst und beginnt von vorne.
#!/usr/bin/env bash
set -euo pipefail
readonly SNAPSHOT="/backups/state/app.snapshot"
readonly ARCHIVE="/backups/app-$(date +%Y%m%d).tar.gz"
# Force a fresh full backup every Sunday by resetting the snapshot file
if [[ "$(date +%u)" == "7" ]]; then
rm -f "$SNAPSHOT"
echo "Sunday: forcing a full backup"
fi
tar --listed-incremental="$SNAPSHOT" -czf "$ARCHIVE" -C /var/www app
4. Fehlerbehandlung bei vollem Speicher: früh erkennen statt spät scheitern
Ein voller Datenträger während des Schreibens einer Archivdatei ist einer der gefährlichsten Fehlerfälle, weil tar mit set -e zwar den Exit-Code korrekt setzt, aber die bereits teilweise geschriebene Archivdatei am Zielort liegen bleibt und je nach Zeitpunkt des Abbruchs sogar noch scheinbar gültig aussehen kann. Ein robustes Skript prüft deshalb vor dem Start bereits, ob genügend freier Speicherplatz vorhanden ist, statt das Problem erst mitten im Schreibvorgang zu bemerken.
Zusätzlich gehört eine Nachprüfung nach dem Schreiben in jedes Backup-Skript: Die fertige Archivdatei sollte mit tar -tzf auf Lesbarkeit geprüft werden, bevor sie als gültiges Backup markiert wird, und eine unvollständige Datei aus einem abgebrochenen Lauf gehört konsequent gelöscht statt am Zielort belassen, damit spätere Wiederherstellungsversuche nicht auf ein scheinbar vorhandenes, aber tatsächlich kaputtes Archiv stoßen.
#!/usr/bin/env bash
set -euo pipefail
readonly ARCHIVE="/backups/app-$(date +%Y%m%d).tar.gz"
readonly DEST_DIR="/backups"
readonly MIN_FREE_MB=2048
cleanup_on_failure() {
[[ -f "$ARCHIVE" ]] && rm -f "$ARCHIVE"
echo "Backup FAILED, removed incomplete archive: $ARCHIVE" >&2
}
trap cleanup_on_failure ERR
# Fail fast if there is not enough free space before starting
free_mb=$(df --output=avail -m "$DEST_DIR" | tail -n1 | tr -d ' ')
if (( free_mb < MIN_FREE_MB )); then
echo "Not enough free space: ${free_mb}MB available, ${MIN_FREE_MB}MB required" >&2
exit 1
fi
tar -czf "$ARCHIVE" -C /var/www app
# Verify the archive is actually readable before trusting it
if ! tar -tzf "$ARCHIVE" > /dev/null; then
echo "Archive verification FAILED: $ARCHIVE is corrupt" >&2
exit 1
fi
echo "Backup verified OK: $ARCHIVE"
5. Kompressions-Level vs. Laufzeit: den richtigen Tradeoff wählen
gzip bietet über die Option -1 bis -9 eine direkte Kontrolle über den Kompressionsgrad, wobei -1 die schnellste, aber am wenigsten komprimierende Stufe ist und -9 die höchste Kompression bei deutlich höherem Rechenaufwand liefert. Der Standardwert liegt bei -6 und stellt für die meisten Anwendungsfälle bereits einen vernünftigen Kompromiss dar, aber für ein Backup-Skript lohnt sich eine bewusste Entscheidung statt des Defaults.
Für sehr große Datenmengen, bei denen die Backup-Laufzeit in ein enges Zeitfenster passen muss, ist eine niedrigere Kompressionsstufe wie -3 oft die bessere Wahl, weil die eingesparte Rechenzeit den zusätzlichen Speicherbedarf der etwas größeren Archivdatei meist aufwiegt. Für Archive, die selten geschrieben, aber häufig über eine langsame Netzwerkverbindung übertragen oder über lange Zeiträume gespeichert werden, überwiegt dagegen der Vorteil einer hohen Kompressionsstufe.
#!/usr/bin/env bash
set -euo pipefail
# Fast backup within a tight maintenance window: lower compression
tar -cf - -C /var/www app | gzip -3 > /backups/app-fast.tar.gz
# Long-term archive storage: higher compression, more CPU time accepted
tar -cf - -C /var/www app | gzip -9 > /backups/app-archive.tar.gz
6. Kompression parallelisieren, um den Tradeoff zu entschärfen
Ein häufig übersehener Ausweg aus dem Kompromiss zwischen Laufzeit und Kompressionsgrad ist, die Kompression selbst auf mehrere CPU-Kerne zu verteilen, statt sich zwischen einer niedrigen Stufe und langer Laufzeit entscheiden zu müssen. Werkzeuge wie pigz sind ein direkter Drop-in-Ersatz für gzip mit identischer Kommandozeilen-Syntax, nutzen aber alle verfügbaren Kerne parallel und erreichen dieselbe Kompressionsstufe in einem Bruchteil der Zeit.
Wichtig ist, ein Skript so zu schreiben, dass es bei fehlendem pigz automatisch auf das immer verfügbare gzip zurückfällt, damit dasselbe Skript auf einem minimalen Server ohne zusätzliche Pakete genauso funktioniert wie auf einem gut ausgestatteten Backup-Host. Diese Rückfalllogik kostet nur wenige Zeilen, verhindert aber, dass ein Skript auf einem System ohne pigz einfach mit einem Fehler abbricht.
#!/usr/bin/env bash
set -euo pipefail
# Use pigz (parallel gzip) if available, fall back to plain gzip otherwise
compressor() {
if command -v pigz &> /dev/null; then
pigz -9
else
gzip -9
fi
}
tar -cf - -C /var/www app | compressor > /backups/app-parallel.tar.gz
7. Backups regelmäßig testweise wiederherstellen
Ein Archiv, das mit tar -tzf als lesbar geprüft wurde, garantiert nur, dass die Struktur intakt ist, nicht dass jede einzelne Datei tatsächlich fehlerfrei wiederhergestellt werden kann und dass die Anwendung mit dem wiederhergestellten Zustand tatsächlich funktioniert. Die einzige Prüfung, die dieses Vertrauen wirklich rechtfertigt, ist eine regelmäßige, echte Wiederherstellung in eine isolierte Testumgebung, gefolgt von einem einfachen funktionalen Check.
Ein solcher Restore-Test lässt sich problemlos automatisieren und wöchentlich oder monatlich als eigener Cron-Job laufen lassen, der ein zufällig ausgewähltes Backup entpackt, gegen eine erwartete Dateiliste prüft und das Ergebnis an dasselbe Monitoring-System meldet wie der eigentliche Backup-Lauf. Ein Team, das erst beim tatsächlichen Notfall zum ersten Mal ein Backup einspielt, entdeckt Probleme immer zum denkbar schlechtesten Zeitpunkt.
8. Aufbewahrungsfristen automatisiert durchsetzen
Ohne automatisierte Bereinigung wachsen Backup-Verzeichnisse unbegrenzt und laufen früher oder später selbst in den vollen Speicher, den das Skript eigentlich behandeln soll. Eine einfache Aufbewahrungsregel, etwa tägliche Backups sieben Tage und wöchentliche Vollbackups zwölf Wochen zu behalten, lässt sich mit find anhand des Datei-Änderungsdatums zuverlässig durchsetzen.
Diese Bereinigung sollte immer nach der erfolgreichen Verifikation des neuen Backups laufen, niemals davor, damit im Fehlerfall eines fehlgeschlagenen neuen Laufs das letzte funktionierende Backup nicht versehentlich mitgelöscht wird. Diese Reihenfolge ist der wichtigste Unterschied zwischen einer Bereinigung, die im Ernstfall hilft, und einer, die im Ernstfall zusätzlichen Schaden anrichtet.
#!/usr/bin/env bash
set -euo pipefail
readonly BACKUP_DIR="/backups"
readonly KEEP_DAYS=7
# Only prune AFTER the new backup was verified successfully
find "$BACKUP_DIR" -maxdepth 1 -name '*.tar.gz' -mtime "+${KEEP_DAYS}" -print -delete
9. Backup-Strategien im Vergleich
Die Wahl zwischen reinem Vollbackup, reinem Inkrementalbackup und einer Kombination aus beiden hängt vor allem von der Größe der Datenmenge, der Häufigkeit von Änderungen und den Anforderungen an die Wiederherstellungszeit ab. Für kleine bis mittlere Datenmengen mit ausreichendem Zeitfenster bleibt ein tägliches Vollbackup oft die einfachste und zuverlässigste Lösung, weil jede Wiederherstellung nur ein einziges Archiv benötigt.
| Strategie | Backup-Laufzeit | Wiederherstellungs-Komplexität | Typischer Einsatz |
|---|---|---|---|
| Tägliches Vollbackup | Hoch, unabhängig von Änderungen | Niedrig, ein einziges Archiv | Kleine bis mittlere Datenmengen |
| Reines Inkrementalbackup | Niedrig | Hoch, alle Inkremente in Reihenfolge | Sehr große, selten wiederherzustellende Daten |
| Wöchentlich voll + täglich inkrementell | Ausgewogen | Mittel, ein Voll- plus wenige Inkremente | Die meisten produktiven Serverumgebungen |
| Parallele Kompression (pigz) | Deutlich reduziert | Unverändert | Große Datenmengen mit engem Zeitfenster |
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Shell-Skripte, die in der Produktion zuverlässig laufen?
Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.
Code-Review
ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.
Refactoring
Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.
CI-Integration
ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.
10. Zusammenfassung
tar- und gzip-Automatisierung in Bash: Das Wichtigste auf einen Blick
Fehler früh erkennen
Freien Speicherplatz vor dem Start prüfen, set -euo pipefail nutzen und die fertige Archivdatei mit tar -tzf verifizieren, bevor sie als gültig gilt.
Inkrementell mit Snapshot-Datei
--listed-incremental spart Zeit und Platz, die Snapshot-Datei muss aber dauerhaft und getrennt vom Archivziel aufbewahrt werden.
Kompression bewusst wählen
Niedrigere gzip-Stufen sparen Zeit bei engen Zeitfenstern, pigz nutzt mehrere CPU-Kerne und entschärft den Tradeoff fast vollständig.
Aufräumen erst nach Verifikation
Alte Backups erst löschen, nachdem das neue Backup erfolgreich verifiziert wurde, sonst geht im Fehlerfall das letzte funktionierende Backup verloren.