Automatisierte Server-Sicherung mit Rsync, Tar und HTML-Reports   Vor kurzem aktualisiert!


Man hofft Backups nie zu brauchen, aber wenn doch, müssen sie zu 100 % funktionieren. In diesem Beitrag geht es um ein Bash-Skript, das Backups von Remote-Servern verwaltet, differenzielle Sicherungen erstellt und per HTML-Mail über den Status auf dem Laufenden hält.

Screenshot Report E-Mail Backup
Screenshot E-Mail-Report Backup

Das Konzept: Remote-Pull-Backup

Im Gegensatz zu Backups, die vom Quellserver „gepusht“ werden, nutzt dieses Skript das Pull-Prinzip. Ein zentraler Backup-Server steuert den Prozess:

  1. Zentraler Start: Das Skript läuft auf dem Backup-System (z. B. via Cronjob).
  2. Sicherer Login: Per SSH loggt sich das Skript auf dem Zielserver ein.
  3. Effizienter Abgleich: Nur geänderte Daten werden übertragen.

Die Architektur im Überblick

Die Logik ist zweistufig aufgebaut, um Speicherplatz zu sparen und dennoch eine schnelle Wiederherstellung zu ermöglichen.

PhaseToolBeschreibung
SynchronisationrsyncSpiegelt das Dateisystem des Remote-Servers 1:1 in ein lokales Verzeichnis.
ArchivierungtarErstellt komprimierte Abbilder (.tgz). Sonntags ein Full-Backup (Level 0), an Wochentagen nur die Änderungen (Differenziell).
ReportingsendmailGeneriert eine HTML-E-Mail mit Statusfarben und Speicherplatz-Statistik.

Kernfunktionen des Skripts

  • Plausibilitäts-Check: Das Skript prüft nach der Archivierung, ob die Datei existiert und größer als 0 Byte ist. Ein abgebrochener Prozess wird sofort als FEHLER markiert.
  • Dynamische Parameter: Servername und Host-Adresse werden als Argumente übergeben. Ein Skript bedient somit beliebig viele Server. Für ein zusätzliches manuelles Level 0 / Full-Backup kann man auch den Level als Parameter übergeben.
  • Bandbreiten-Limitierung: Durch --bwlimit wird verhindert, dass das Backup während der Geschäftszeiten die Internetverbindung blockiert.
  • Intelligentes Log-Parsing: Die E-Mail enthält die letzten Zeilen des Rsync-Logs, sodass man sofort sieht, welche Dateien übertragen wurden.

Das Skript (Anonymisierte Vorlage)

Hier ist die Struktur des Skripts zum Nachbauen. Pfade und Adressen wurden durch Platzhalter ersetzt.

#!/bin/bash
# ==============================================================================
# Remote-Backup-Skript mit Rsync, Tar-Differenz und HTML-Reporting
# ------------------------------------------------------------------------------
# Funktionsweise: 
# 1. Sync per Rsync (Remote Pull)
# 2. Archivierung per Tar (Sonntag: Full, Wochentag: Differenziell)
# 3. Statusbericht per HTML-E-Mail
# ==============================================================================

# --- 1. KONFIGURATION (Bitte anpassen) ---
PATH=/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MNT="/mnt/backup-storage"              # Lokaler Pfad zum Backup-Speicher
NAME="${1:-meinserver}"                # Kurzname für Logs und Dateien
REMOTE_HOST="${2:-remote.example.com}" # Hostname oder IP des Zielservers
SSH_PORT="22"                          # SSH-Port des Zielservers
SENDER="backup@deine-domain.de"        # Absender der Status-Mail
RECEIVER="admin@deine-domain.de"       # Empfänger der Status-Mail

# Ausschlüsse für das Backup (Pfade auf dem Remote-Server)
EXCLUDES=(
    --exclude /proc/ --exclude /sys/ --exclude /dev/ 
    --exclude /tmp/ --exclude /run/ --exclude /mnt/ 
    --exclude /media/ --exclude /backup/ --exclude /var/cache/
)

# Variablen für Logs und Temp-Dateien
DIR="$MNT/$NAME"
RSYNC_LOG="$MNT/$NAME-rsync.log"
TMP_MAIL=$(mktemp /tmp/backup_mail.XXXXXX.html)
DAY=$(date +%w) # 0=Sonntag (Full-Backup), 1-6=Diff-Backup

# --- 2. HILFSFUNKTIONEN ---
log_event() {
    local msg="$1"
    echo "$(date '+%d.%m.%Y %H:%M:%S') : $msg" >> "$MNT/backup_global.log"
    logger -t "Backup-$NAME" "$msg"
}

get_size() {
    [ -f "$1" ] && du -sh "$1" | cut -f1 || echo "0"
}

# --- 3. PRÜFUNG & START ---
[ -d "$DIR" ] || mkdir -p "$DIR"
log_event "Backup-Start für $NAME (Level $DAY) auf $REMOTE_HOST"

RSYNC_STATUS="Fehlgeschlagen"
TAR_STATUS="Wartet..."
COLOR_RSYNC="#d9534f" # Rot
COLOR_TAR="#777777"   # Grau

# --- 4. RSYNC SYNCHRONISATION ---
# Wir holen die Daten vom Remote-Server in unser lokales Verzeichnis
rsync -avzHPWAX --delete-after --bwlimit=1000 "${EXCLUDES[@]}" \
      -e "ssh -p $SSH_PORT" root@"$REMOTE_HOST":/ "$DIR/" > "$RSYNC_LOG" 2>&1

if [ $? -eq 0 ]; then
    RSYNC_STATUS="OK"
    COLOR_RSYNC="#5cb85c" # Grün
    touch "$DIR/.stamp"
else
    log_event "KRITISCH: Rsync-Fehler bei $NAME. Archivierung wird dennoch versucht."
fi

# --- 5. TAR ARCHIVIERUNG (Plausibilisierung) ---
cd "$DIR" || exit
ARCHIVE="$MNT/backup-$NAME-$DAY.tgz"
LAST_FULL="$MNT/backup-$NAME-0.tgz"

if [ "$DAY" = "0" ]; then
    # Sonntag: Full Backup. Altes Archiv wird zur Vorwoche rotiert.
    [ -f "$ARCHIVE" ] && mv "$ARCHIVE" "$MNT/backup-$NAME-lastweek.tgz"
    tar cvfz "$ARCHIVE" ./ > /dev/null 2>&1
else
    # Wochentag: Differenzielles Backup bezogen auf Level 0
    if [ -f "$LAST_FULL" ]; then
        find ./ -cnewer "$LAST_FULL" -type f > "$MNT/$NAME-tar-list.log"
        tar cvfz "$ARCHIVE" -T "$MNT/$NAME-tar-list.log" > /dev/null 2>&1
    else
        log_event "Warnung: Kein Full-Backup gefunden. Erstelle Level 0..."
        tar cvfz "$ARCHIVE" ./ > /dev/null 2>&1
    fi
fi

# Prüfung: Existiert das Archiv und hat es Inhalt?
if [ -s "$ARCHIVE" ]; then
    TAR_STATUS="OK"
    COLOR_TAR="#5cb85c"
else
    TAR_STATUS="FEHLER (Archiv leer oder fehlt)"
    COLOR_TAR="#d9534f"
fi

# --- 6. HTML E-MAIL BERICHT ERSTELLEN ---
cat <<EOF > "$TMP_MAIL"
<html>
<head>
<style>
    body { font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; }
    .card { background: #fff; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
    h2 { color: #333; border-bottom: 2px solid #0056b3; padding-bottom: 10px; }
    .status { padding: 8px 12px; color: white; border-radius: 3px; font-weight: bold; }
    table { width: 100%; border-collapse: collapse; margin-top: 20px; }
    th, td { text-align: left; padding: 12px; border: 1px solid #eee; }
    th { background: #f8f8f8; }
    pre { background: #222; color: #adff2f; padding: 15px; border-radius: 3px; font-size: 12px; overflow-x: auto; }
</style>
</head>
<body>
    <div class="card">
        <h2>Backup-Status: $NAME</h2>
        <p><strong>Zeitpunkt:</strong> $(date "+%d.%m.%Y %H:%M")<br>
           <strong>Methode:</strong> Level $DAY (0=Full, 1-6=Diff)</p>
        
        <table>
            <tr><th>Prozess</th><th>Status</th></tr>
            <tr><td>Rsync Abgleich</td><td><span class="status" style="background:$COLOR_RSYNC;">$RSYNC_STATUS</span></td></tr>
            <tr><td>Tar Archivierung</td><td><span class="status" style="background:$COLOR_TAR;">$TAR_STATUS</span></td></tr>
        </table>

        <h3>Archiv-Details</h3>
        <ul>
            <li>Datei: <code>$(basename "$ARCHIVE")</code></li>
            <li>Größe: $(get_size "$ARCHIVE")</li>
        </ul>

        <h3>Rsync Log (Auszug)</h3>
        <pre>$(tail -n 8 "$RSYNC_LOG")</pre>
    </div>
</body>
</html>
EOF

# --- 7. VERSAND ---
(
  echo "From: $SENDER"
  echo "To: $RECEIVER"
  echo "Subject: [Backup] $NAME - Status: $TAR_STATUS"
  echo "Content-Type: text/html; charset=UTF-8"
  echo "MIME-Version: 1.0"
  echo ""
  cat "$TMP_MAIL"
) | /usr/sbin/sendmail -t

log_event "Backup $NAME beendet. Status: $TAR_STATUS"
rm "$TMP_MAIL"

Warum diese Lösung?

Viele Standard-Lösungen sind komplex oder bieten zu wenig Feedback. Dieses Skript schließt die Lücke:

  1. Transparenz: Man sieht sofort im Postfach, ob alles „Grün“ ist.
  2. Sicherheit: Durch das Pull-Verfahren muss der Zielserver keine Zugangsdaten zum Backup-Server kennen.
  3. Kontrolle: Man behält die volle Gewalt über Exclude-Listen und Kompressionsraten.

Am besten nutzt man SSH-Keys für den Login, damit das Skript ohne Passworteingabe durchlaufen kann. Man achtet dabei darauf, den SSH-Key auf dem Zielserver idealerweise auf die benötigten Befehle einzuschränken.

Die Automatisierung: Einbindung in Cron

Da das Skript generisch aufgebaut ist, kannst man für jeden Server eine eigene Zeile in die Crontab eintragen. Beispielsweise mit crontab -e . Ein klassisches Setup sieht vor, die Backups in die frühen Morgenstunden zu legen, wenn die Serverlast niedrig ist:

# Jeden Morgen um 01:00 Uhr Backup für Server 'nc5'
00 01 * * * /usr/local/bin/backup-server-generic.sh nc5 nc5.zlauf.de

# Jeden Morgen um 02:00 Uhr Backup für den Webserver
00 02 * * * /usr/local/bin/backup-server-generic.sh web01 web01.beispiel.de

Es kann sinnvoll sein, die Zeiten leicht versetzt zu organisieren, damit der Backup-Server (und die Internetanbindung) nicht alle Systeme gleichzeitig bearbeitet.

Restore einzelner Dateien

Wenn man versehentlich eine wichtige Datei gelöscht hat, musst man nicht das komplette Gigabyte-Archiv entpacken. Dank tar kann man gezielt einzelne Files oder Verzeichnisse „restorieren“.

Zuerst suchst man im Archiv nach der gewünschten Datei:

tar -ztf /pfad/zu/backups/backup-nc5-0.tgz | grep "dateiname"

Nachdem der Pfad bekannt ist, extrahiert man die Datei in ein temporäres Verzeichnis (z. B. /tmp/restore), um sie danach wieder an ihren Platz zu schieben:

# Erstellt die Ordnerstruktur und extrahiert nur die eine Datei
tar -zxvf /pfad/zu/backups/backup-nc5-0.tgz -C /tmp/restore var/www/html/wichtige_datei.php

Wichtig: Da mit relativen Pfaden im Archiv arbeitet wird, musst man beim Entpacken das Zielverzeichnis mit -C angeben, damit tar nicht versehentlich Dateien im aktuellen Verzeichnis überschreibt. Erst prüfen, dann verschieben!