Mein Backup war komplett — aber der Restore schlug fehl
Stille Korruption: Der Crash kam erst beim Restore.
Incident
Montag, 7:02 Uhr. Die SD-Karte meines Kleinservers im Alltag gab plötzlich auf. Ich wechselte die Hardware, spielte das letzte SQLite-Backup ein — und bekam sofort einen Fehler:
Error: database disk image is malformed
Das Backup war von letzter Nacht, 2,1 GB groß, vollständig. Aber der Restore brach ab. Was half: ein separates WAL-File aus /var/lib/service/, das zufällig noch auf der alten SD-Karte lag und das ich ebenfalls kopiert hatte.
Root Cause
Der Server lief mit SQLite im WAL-Modus (journal_mode=WAL). Mein Backup-Skript kopierte die Datenbankdatei mit cp db.sqlite /backup/, ohne WAL-Shm oder WAL zu berücksichtigen. Das resultierte in einer stillschweigend korrupten Kopie: die Hauptdatei war aktuell, aber das WAL enthielt Transaktionen, die noch nicht eingecheckt waren. Ohne das dazugehörige WAL ließ sich die Datenbank nicht mehr öffnen.
Warum half der Workaround mit dem alten WAL-File nur teilweise? Weil zwischen Backup-Zeitpunkt und SD-Karten-Crash weitere Transaktionen fehlten. Das Backup war damit unbrauchbar.
Lösung
SQLite bietet ein eigenes Online-Backup-API, das konsistente Kopien auch bei parallelen Schreibzugriffen erstellt. Für die Kommandozeile reicht der .backup-Befehl des sqlite3-Clients.
#!/usr/bin/env bash
# backup.sh — konsistentes SQLite-Backup im WAL-Modus
set -euo pipefail
DB="/var/lib/myapp/app.sqlite"
BACKUP_DIR="/backup/sqlite"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/app_${TIMESTAMP}.sqlite"
mkdir -p "$BACKUP_DIR"
# Nutzt SQLites Online-Backup-API statt raw cp
sqlite3 "$DB" ".backup '${BACKUP_FILE}'"
# Optional: Auf Korruption prüfen
if ! sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;" | grep -q 'ok'; then
echo "Backup-Korruption erkannt!" >&2
rm -f "$BACKUP_FILE"
exit 1
fi
Warum das funktioniert: .backup kopiert die Datenbank Seite für Seite über SQLites internen Mechanismus. WAL und Shm-Dateien werden berücksichtigt, und die Kopie ist in einem konsistenten Zustand — egal ob gerade geschrieben wird oder nicht.
Für automatisierte Umgebungen gibt es alternativ eine kurze Go-Helferfunktion:
// backup.go — minimaler Online-Backup über Go
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
"log"
"os"
)
func backup(src, dst string) error {
db, err := sql.Open("sqlite3", "file:"+src+"?_journal_mode=WAL")
if err != nil {
return err
}
defer db.Close()
backup, err := os.Create(dst)
if err != nil {
return err
}
defer backup.Close()
_, err = db.Exec("BACKUP TO ?", dst)
return err
}
func main() {
if err := backup("/var/lib/myapp/app.sqlite", "/backup/app.sqlite"); err != nil {
log.Fatal(err)
}
}
Für systemd-Umgebungen bietet sich zudem ein entsprechender Timer an:
# /etc/systemd/system/sqlite-backup.timer
[Unit]
Description=Tägliches SQLite-Backup
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/sqlite-backup.service
[Unit]
Description=SQLite Online-Backup
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Verifikation
- Konsistenztest nach jedem Backup:
sqlite3 /backup/app_20260819.sqlite "PRAGMA integrity_check;"
Erwartete Ausgabe: ok
- Restore-Test in einer Sandbox:
# Neue Instanz starten
sqlite3 /tmp/test_restore.db ".restore /backup/app_20260819.sqlite"
# Dann öffnen und prüfen
sqlite3 /tmp/test_restore.db "SELECT count(*) FROM wichtige_tabelle;"
- Langfristiger Check: Prüfe monatlich, ob
sqlite3 backup.db "PRAGMA integrity_check;"sauber durchläuft.
Fazit
Ein Backup, das sich nicht wiederherstellen lässt, ist kein Backup — sondern nur eine Illusion von Sicherheit. SQLite im WAL-Modus braucht zwingend das Online-Backup-API; cp reicht nicht aus. Seit ich das Skript umgestellt habe, schlafe ich ruhiger — und die nächste Wiederherstellung hat funktioniert.
Metrik: Restore-Dauer für eine 2,1 GB-Datenbank: etwa 45 Sekunden. Vorher: Backup 8 Sekunden, Restore unmöglich. Jetzt: Backup 40 Sekunden, Restore 45 Sekunden — dafür konsistent.
Offene Frage: Nutzt ihr sqlite3 .backup oder ein externes Tool wie litestream für Replikation? Ich bin neugierig, wie ihr Produktions-Backups für kleine Dienste gestaltet.
Siehe auch: