indi-allsky Backup auf FRITZ!NAS Version 2.0: So sichere ich heute Datenbank, Konfiguration und System

Ende 2025 hatte ich für meine indi-allsky Installation auf dem Raspberry Pi ein zweistufiges Backup eingerichtet: Zunächst wurden Datenbank, Konfiguration und Migrationen gesichert, anschließend die Bilddateien und Timelapses. Das hat grundsätzlich funktioniert, war mit der Zeit aber deutlich aufwendiger als nötig.

Inzwischen landen die Timelapses zusätzlich auf meinem YouTube-Kanal. Die großen Bildbestände will ich nicht mehr täglich durch das lokale Netzwerk auf die FRITZ!Box schieben. Viel wichtiger ist mir, dass ich nach einem Defekt der NVMe-SSD die laufende indi-allsky Installation mit möglichst wenig Aufwand wieder aufbauen kann.

Deshalb habe ich das Backup im Oktober 2026 neu aufgesetzt. Die bisherigen Artikel beschreiben ausdrücklich die alte Variante: Phase 1: Datenbank, Konfiguration und Migrationen und Phase 2: Dateien und qualitätsbasiertes Backup. Beide Cronjobs sind bei mir inzwischen deaktiviert.

Warum ich das Backup umgestellt habe

Die alte Lösung hat unter anderem einen täglichen SQLite-Dump direkt auf das SMB-Laufwerk geschrieben. Als die externe SSD voll war, schlugen diese Backups fehl. Außerdem brachte das Kopieren und Löschen großer Bildmengen die FRITZ!Box spürbar ins Schwitzen.

Mein neues Prinzip ist einfacher: indi-allsky erstellt selbst eine konsistente SQLite-Sicherung. Ein separates Script kontrolliert diese und kopiert sie auf die FRITZ.NAS. Ein zweiter, wöchentlicher Job archiviert die wichtigen Systemdateien. Die eigentlichen Fotos und Timelapses werden dabei bewusst nicht mitgesichert.

Mein Aufbau

  • Raspberry Pi mit Debian 13 und indi-allsky
  • System auf einer internen NVMe-SSD
  • Externe SanDisk SSD an einer FRITZ!Box 6690 Cable
  • Einbindung über SMB/CIFS unter /mnt/backup_allsky
  • Täglich um 03:00 Uhr: Datenbank und Konfiguration
  • Sonntags um 04:00 Uhr: Systemsicherung
  • Benachrichtigung per E-Mail nur bei Fehlern

Die lokale indi-allsky-Datenbanksicherung bewahrt sieben Generationen auf. Auf der FRITZ.NAS bleiben die täglichen Sicherungen 25 Tage erhalten; bei der wöchentlichen Systemsicherung behalte ich vier Generationen.

1. Die integrierte Datenbanksicherung aktivieren

Im indi-allsky-Webinterface unter den Backup-Einstellungen habe ich DB BACKUP FREQUENCY (DAYS) auf 1 gesetzt. indi-allsky erstellt damit regelmäßig eine komprimierte SQLite-Sicherung unter /var/lib/indi-allsky/backup/. Die integrierte Funktion nutzt die SQLite-Backup-API und ist deshalb gegenüber einem selbst gebauten Dump der laufenden Datenbank vorzuziehen.

Eine manuelle Sicherung lässt sich so anstoßen:

cd ~/indi-allsky
./misc/indi-allsky-ctl backup-db

Wichtig: Die Uhrzeit der internen Sicherung ist nicht fest an den NAS-Cronjob gekoppelt. Deshalb prüft das externe Script, ob die jüngste lokale Sicherung höchstens 48 Stunden alt ist.

2. FRITZ.NAS anbinden und Verzeichnisse anlegen

Den SMB-Mount hatte ich schon in der alten Anleitung beschrieben. Hier nur die kurze Kontrolle: Der Mount muss tatsächlich aktiv sein. Ein bloß vorhandener lokaler Ordner reicht nicht, sonst würde man versehentlich auf die eigene SSD sichern.

mountpoint /mnt/backup_allsky
findmnt /mnt/backup_allsky

Meine neue Verzeichnisstruktur:

backup_allsky/
├── database/       # SQLite-Backups
├── configuration/  # Konfiguration und Migrationen
└── system/         # Woechentliche Systemsicherung

Die FRITZ!Box kann zusätzlich selbst einen Ordner FRITZ anlegen. Den lasse ich in Ruhe. Die eigentlichen Verzeichnisse lassen sich so erstellen:

sudo mkdir -p /mnt/backup_allsky/backup_allsky/{database,configuration,system}

3. Das tägliche NAS-Backup

Das folgende Script sucht das neueste interne indi-allsky-Backup, kontrolliert GZIP und SQLite, erstellt Archive für Konfiguration und Migrationen und überträgt alles auf die FRITZ.NAS. Dabei wird jede Kopie per cmp mit dem lokalen Original verglichen. Erst danach werden alte Sicherungen bereinigt. Pro Kategorie bleibt mindestens die jüngste Sicherung erhalten.

Den E-Mail-Empfänger im Script bitte anpassen. Voraussetzung ist ein funktionierender msmtp-Versand unter root. Ich nutze Port 587 mit STARTTLS; Passwörter gehören ausschließlich in die geschützte Mailkonfiguration, nicht in dieses Script.

Hinweis zum Code: Der folgende Stand enthält eine kleine Verbesserung gegenüber meiner ersten Fassung: Der Fehleralarm hängt an EXIT, damit auch ein explizites exit 1 eine Meldung auslöst. Diese verbesserte Fassung sollte vor dem Produktiveinsatz separat getestet werden.

Script speichern als /usr/local/bin/backup_allsky_nas.sh:

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

NAS="/mnt/backup_allsky"
ROOT="$NAS/backup_allsky"
SOURCE="/var/lib/indi-allsky/backup"
MAIL_TO="mail@BEISPIEL.DE"
LOG="/var/log/backup_allsky_nas.log"
RETENTION=25
WORK=""

exec >>"$LOG" 2>&1

# EXIT statt nur ERR: auch explizites exit 1 und fehlende Dateien melden.
finish() {
    rc=$?
    trap - EXIT
    if [[ -n "$WORK" && -d "$WORK" ]]; then
        rm -rf -- "$WORK"
    fi
    if (( rc != 0 )); then
        {
            printf 'To: %s\n' "$MAIL_TO"
            printf 'Subject: ALARM - Allsky NAS Backup fehlgeschlagen\n'
            printf 'Content-Type: text/plain; charset=UTF-8\n\n'
            printf 'Backup fehlgeschlagen auf %s um %s (Code %s).\n\n' \
                "$(hostname)" "$(date)" "$rc"
            tail -n 35 "$LOG" || true
        } | /usr/bin/msmtp -a default "$MAIL_TO" || \
            echo 'WARNUNG: Fehler-Mail nicht versendet'
    fi
    exit "$rc"
}
trap finish EXIT

echo "=== NAS-Backup: $(date) ==="
mountpoint -q "$NAS"
[[ "$(findmnt -n -o FSTYPE "$NAS")" == "cifs" ]]
for d in "$ROOT" "$ROOT/database" "$ROOT/configuration" "$ROOT/system"; do
    test -d "$d"
done

LATEST="$(find "$SOURCE" -maxdepth 1 -type f \
    -name 'backup_indi-allsky_*.sqlite.gz' -printf '%T@ %p\n' \
    | sort -nr | head -1 | cut -d' ' -f2-)"
test -n "$LATEST" && test -s "$LATEST"
NAME="$(basename "$LATEST")"
NOW="$(date +%s)"
MTIME="$(stat -c %Y "$LATEST")"
if (( NOW - MTIME > 172800 )); then
    echo 'FEHLER: Lokales Datenbankbackup ist aelter als 48 Stunden'
    exit 1
fi

WORK="$(mktemp -d /var/tmp/allsky-backup-XXXXXXXX)"
gzip -t "$LATEST"
gzip -dc "$LATEST" > "$WORK/test.sqlite"
RESULT="$(sqlite3 -readonly "$WORK/test.sqlite" 'PRAGMA integrity_check;')"
[[ "$RESULT" == 'ok' ]] || { echo "FEHLER: SQLite: $RESULT"; exit 1; }
rm -f -- "$WORK/test.sqlite"

tar -C /etc -czf "$WORK/configuration.tgz" indi-allsky
tar -tzf "$WORK/configuration.tgz" >/dev/null
tar -C /var/lib/indi-allsky/migrations \
    -czf "$WORK/migrations.tgz" versions
tar -tzf "$WORK/migrations.tgz" >/dev/null

transfer() {
    local src="$1" dst="$2" tmp="${2}.part"
    rm -f -- "$tmp"
    cp -- "$src" "$tmp"
    cmp -s -- "$src" "$tmp"
    mv -f -- "$tmp" "$dst"
    echo "Verifiziert: $(basename "$dst")"
}
STAMP="$(date +%Y%m%d_%H%M%S)"
transfer "$LATEST" "$ROOT/database/$NAME"
transfer "$WORK/configuration.tgz" \
    "$ROOT/configuration/configuration_${STAMP}.tgz"
transfer "$WORK/migrations.tgz" \
    "$ROOT/configuration/migrations_${STAMP}.tgz"

# Nach erfolgreichem Transfer maximal 25 Tage behalten;
# jeweils die neueste Datei bleibt auch bei Ablauf erhalten.
cleanup_old() {
    local dir="$1" pattern="$2"
    local -a files=()
    mapfile -d '' -t files < <(find "$dir" -maxdepth 1 -type f \
        -name "$pattern" -printf '%T@ %p\0' | sort -zrn)
    local i path entry
    for ((i=1; i<${#files[@]}; i++)); do
        entry="${files[$i]}"
        path="${entry#* }"
        if (( $(date +%s) - $(stat -c %Y "$path") > RETENTION * 86400 )); then
            rm -f -- "$path"
        fi
    done
}
cleanup_old "$ROOT/database" 'backup_indi-allsky_*.sqlite.gz'
cleanup_old "$ROOT/configuration" 'configuration_*.tgz'
cleanup_old "$ROOT/configuration" 'migrations_*.tgz'
echo "=== NAS-BACKUP ERFOLGREICH: $(date) ==="
sudo chmod 700 /usr/local/bin/backup_allsky_nas.sh
sudo bash -n /usr/local/bin/backup_allsky_nas.sh
sudo flock -n /var/lock/backup_allsky_nas.lock /usr/local/bin/backup_allsky_nas.sh
sudo tail -n 35 /var/log/backup_allsky_nas.log

Bei erfolgreicher Prüfung erscheint am Ende NAS-BACKUP ERFOLGREICH. Erst danach wird der Cronjob unter sudo crontab -e eingetragen:

0 3 * * * /usr/bin/flock -n /var/lock/backup_allsky_nas.lock /usr/local/bin/backup_allsky_nas.sh

4. Ergänzend: Wöchentliche Systemsicherung

Die Datenbank enthält zwar viele Einstellungen, aber nicht alles, was meinen Raspberry Pi ausmacht. Insbesondere YouTube-OAuth, eigene Scripts, ADS-B, Systemd-Dienste, Cronjobs und Boot-Konfiguration sollen nach einem SSD-Ausfall nicht verloren sein.

Ich sichere deshalb sonntags ein gezieltes Systemarchiv. Python-Virtualenvs, Build-Caches, Bilder und Videos lasse ich weg. Die verwendete indi-allsky-Git-Version sowie installierte Pakete und Cronjobs werden dokumentiert. Das ist kein bootfähiges SSD-Image: Im Ernstfall würde ich Debian und indi-allsky neu installieren und die Konfiguration zurückspielen.

Script speichern als /usr/local/bin/backup_allsky_system.sh:

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
NAS="/mnt/backup_allsky"
DEST="$NAS/backup_allsky/system"
LOG="/var/log/backup_allsky_system.log"
MAIL="mail@BEISPIEL.DE"
WORK=""

finish() {
    rc=$?
    trap - EXIT
    if [[ -n "$WORK" && -d "$WORK" ]]; then
        rm -rf -- "$WORK"
    fi
    if (( rc != 0 )); then
        {
            printf 'Subject: ALARM - Allsky Systembackup fehlgeschlagen\n\n'
            printf 'Host: %s\nZeit: %s\nExit-Code: %s\n\n' \
                "$(hostname)" "$(date)" "$rc"
            tail -n 30 "$LOG" || true
        } | /usr/bin/msmtp -a default "$MAIL" || true
    fi
    exit "$rc"
}
trap finish EXIT
exec >>"$LOG" 2>&1

echo "=== Systembackup gestartet: $(date) ==="
mountpoint -q "$NAS"
[[ "$(findmnt -n -o FSTYPE "$NAS")" == 'cifs' ]]
test -d "$DEST"
WORK="$(mktemp -d /var/tmp/allsky-system-XXXXXXXX)"
STAMP="$(date +%Y%m%d_%H%M%S)"
ARCHIVE="allsky-system_${STAMP}.tar.gz"
mkdir -p "$WORK/restore-info"

dpkg-query -W -f='${binary:Package} ${Version}\n' \
    > "$WORK/restore-info/packages.txt"
crontab -l > "$WORK/restore-info/root-crontab.txt"
crontab -u dante -l > "$WORK/restore-info/dante-crontab.txt"
git -C /home/dante/indi-allsky rev-parse HEAD \
    > "$WORK/restore-info/indi-allsky-commit.txt"
systemctl list-unit-files --state=enabled --no-pager \
    > "$WORK/restore-info/system-services.txt"
find /home/dante/.config/systemd/user -maxdepth 2 \
    \( -type f -o -type l \) -print \
    > "$WORK/restore-info/user-services.txt"
/home/dante/Projects/adsb-project/venv/bin/python -m pip freeze \
    > "$WORK/restore-info/adsb-requirements.txt"

tar -C / -czf "$WORK/$ARCHIVE" \
    --exclude='home/dante/indi-allsky/virtualenv' \
    --exclude='home/dante/indi-allsky/.git' \
    --exclude='home/dante/Projects/adsb-project/venv' \
    --exclude='home/dante/Projects/adsb-project/logs' \
    --exclude='home/dante/Projects/adsb-project/out' \
    --exclude='home/dante/.cache' \
    --exclude='home/dante/indi-allsky/log' \
    --exclude='home/dante/indi-allsky/html' \
    --exclude='home/dante/indi-allsky/content' \
    --exclude='home/dante/indi-allsky/testing' \
    --exclude='home/dante/indi-allsky/examples' \
    boot/firmware etc root \
    home/dante/.config home/dante/.indi \
    home/dante/indi-allsky-watchdog.sh \
    home/dante/youtube-client-secret.json \
    home/dante/indi-allsky \
    home/dante/Projects/adsb-project \
    usr/local/etc usr/local/bin usr/local/lib

tar -C "$WORK" -czf "$WORK/restore-info.tar.gz" restore-info
tar -tzf "$WORK/$ARCHIVE" >/dev/null
tar -tzf "$WORK/restore-info.tar.gz" >/dev/null

transfer() {
    local src="$1" dst="$2" tmp="${2}.part"
    rm -f -- "$tmp"
    cp -- "$src" "$tmp"
    cmp -s -- "$src" "$tmp"
    mv -f -- "$tmp" "$dst"
}
transfer "$WORK/$ARCHIVE" "$DEST/$ARCHIVE"
transfer "$WORK/restore-info.tar.gz" \
    "$DEST/allsky-restore-info_${STAMP}.tar.gz"

# Neueste vier vollstaendige Sicherungsstaende behalten.
mapfile -t archives < <(find "$DEST" -maxdepth 1 -type f \
    -name 'allsky-system_*.tar.gz' -printf '%f\n' | sort -r)
for ((i=4; i<${#archives[@]}; i++)); do
    old="${archives[$i]}"
    suffix="${old#allsky-system_}"
    suffix="${suffix%.tar.gz}"
    rm -f -- "$DEST/$old"
    rm -f -- "$DEST/allsky-restore-info_${suffix}.tar.gz"
done
echo "=== SYSTEMBACKUP ERFOLGREICH: $(date) ==="
sudo chmod 700 /usr/local/bin/backup_allsky_system.sh
sudo bash -n /usr/local/bin/backup_allsky_system.sh
sudo flock -n /var/lock/backup_allsky_system.lock /usr/local/bin/backup_allsky_system.sh
sudo tail -n 35 /var/log/backup_allsky_system.log

Nach erfolgreichem Test folgt auch hier der Eintrag unter sudo crontab -e:

0 4 * * 0 /usr/bin/flock -n /var/lock/backup_allsky_system.lock /usr/local/bin/backup_allsky_system.sh

5. Fehler-E-Mail statt täglicher Erfolgsmeldung

Ich brauche keine Erfolgsmail jeden Morgen. Entscheidend ist, dass ein Ausfall nicht unbemerkt bleibt. Die Scripts schreiben nach /var/log/backup_allsky_nas.log beziehungsweise /var/log/backup_allsky_system.log und schicken bei Fehlern eine E-Mail mit den letzten Logzeilen.

Die systemweite Datei /etc/msmtprc muss vor allem geschützt sein, wenn sie SMTP-Zugangsdaten enthält:

sudo chown root:root /etc/msmtprc
sudo chmod 600 /etc/msmtprc

Ein einfacher Test des Mailversands:

printf 'Subject: Allsky Backup Test\n\nSMTP-Test.\n' | sudo msmtp -a default mail@BEISPIEL.DE

Bei mir war der SMTP-Server erreichbar, aber das hinterlegte Passwort zunächst veraltet. Erst nachdem eine Testmail tatsächlich angekommen war, habe ich mich auf die Fehlerbenachrichtigung verlassen. Auch den Alarmmechanismus selbst habe ich einmal mit einem künstlichen Fehler geprüft.

6. Backups prüfen – und auch einmal eine Datei zurückholen

Der wichtigste Punkt ist für mich nicht, dass ein Script Backup erfolgreich schreibt, sondern dass die Dateien auch verwendbar sind. Beim Datenbankbackup prüfe ich daher die GZIP-Integrität und führe auf einer temporär entpackten SQLite-Kopie PRAGMA integrity_check aus. Die Übertragung auf die FRITZ.NAS wird bytegenau verglichen.

Beim Systembackup habe ich testweise die Apache-Konfiguration aus dem TAR-Archiv in ein temporäres Verzeichnis entpackt und mit der produktiven Datei verglichen. Das Ergebnis: WIEDERHERSTELLUNG ERFOLGREICH. Am laufenden System wurde dabei nichts überschrieben.

Der folgende Test extrahiert ausdrücklich nur eine Datei:

NAS="/mnt/backup_allsky/backup_allsky/system"
BACKUP="$(find "$NAS" -maxdepth 1 -type f -name 'allsky-system_*.tar.gz' \
    -printf '%f\n' | sort -r | head -1)"
[[ -n "$BACKUP" ]] || { echo 'Kein Backup gefunden'; exit 1; }
TESTDIR="$(mktemp -d)"
sudo tar -xzf "$NAS/$BACKUP" -C "$TESTDIR" etc/apache2/apache2.conf
if sudo cmp -s /etc/apache2/apache2.conf "$TESTDIR/etc/apache2/apache2.conf"; then
    echo 'WIEDERHERSTELLUNG ERFOLGREICH'
fi
sudo rm -rf -- "$TESTDIR"

7. Was ich bewusst nicht mehr sichere

In der früheren Phase 2 wurden Bilder und Timelapses nach Sternanzahl und Mondanteil bewertet und auf die externe SSD kopiert. Das ist technisch möglich, bei mir aber nicht mehr Teil des NAS-Backups.

Meine Kamera sammelt große Mengen an Bilddaten. Die Timelapses werden inzwischen zusätzlich auf YouTube veröffentlicht; die Originalbilder liegen weiterhin lokal und werden von indi-allsky nach den dort konfigurierten Aufbewahrungsregeln verwaltet. YouTube ersetzt dabei kein vollständiges Originalbild-Backup. Ich nehme dieses Restrisiko bewusst in Kauf, weil ich die Netzwerklast und den Speicherverbrauch reduzieren möchte.

Fazit

Statt zweier umfangreicher Backup-Phasen habe ich jetzt einen überschaubaren Ablauf: indi-allsky sichert die eigene Datenbank, ein täglicher Job überträgt Datenbank, Konfiguration und Migrationen auf die FRITZ.NAS, und sonntags kommt eine kompakte Systemsicherung dazu. Beide Jobs wurden manuell erfolgreich getestet; auch das Zurückholen einer einzelnen Datei hat funktioniert.

Auf einen vollständigen Restore-Test auf eine neue SSD habe ich bislang verzichtet. Ebenso wichtig: Die Archive enthalten zum Teil Zugangsdaten und sind auf meinem NAS nicht zusätzlich verschlüsselt. Deshalb muss die Freigabe gegen unberechtigten Zugriff abgesichert sein.

Für mich ist das die deutlich praktikablere Lösung: weniger Datenverkehr, nachvollziehbare Prüfungen und eine Nachricht, wenn etwas schiefläuft.

Stand: Oktober 2026. Pfade, Benutzername, SMTP-Empfänger und installierte Komponenten müssen an die eigene indi-allsky-Installation angepasst werden.

Hat dir dieser Beitrag gefallen?

Du kannst allsky-rodgau.de mit einem kleinen Kaffee auf BuyMeACoffee unterstützen.

Jetzt Kaffee spendieren!