# app-backup – geprüfte Sicherungen für den JR IT Server Sichert WordPress, Nextcloud, native Gitea-Installationen mit MariaDB, Maildaten und zusätzliche Verzeichnisse. Jede Komponente erhält ein eigenes Archiv. Uploads gehen ausschließlich über ein **rclone-Crypt-Remote**; anschließend wird der gesamte Inhalt durch dieses Remote zurückgelesen und verglichen. Die vollständige Analyse, Architektur, Konfiguration, Betriebsanleitung und verbleibenden Grenzen stehen in [Dokumentation.md](Dokumentation.md). ## Produktionsstand und verbleibende Abnahme Auf Gemini bestand am 10.09.2026 ein vollständiger Paketlauf mit geschützter Dateikopie, DB-Dumps, OneDrive-Upload, vollständigem Rücklesen und separater Archivprüfung (14 Archive). Dienste und Backup-Timer wurden anschließend gestartet. Das belegt die technische Abnahme für die konfigurierte Umgebung. Eine fachliche Wiederherstellung mit funktionierenden Anwendungen auf einem Testsystem sowie unabhängige Alarmierung und separat verfügbare Crypt-Schlüssel bleiben Voraussetzungen für einen belastbaren Produktionsbetrieb; ihre Abnahme ist noch nicht bestätigt. Ein OneDrive-HTTP-409 wurde im zweiten Upload-Versuch überwunden. Der Code erlaubt standardmäßig bis zu sechs Kopierversuche und veröffentlicht den Remote-Abschlussmarker erst nach erfolgreichem Inhaltsvergleich. Wiederkehrende Konflikte oder endgültige Fehler müssen untersucht werden; ein aktiver Timer allein belegt kein aktuelles Backup. Details: [Fehlerbehandlung und Betriebsgrenzen](Dokumentation.md#upload-fehlerbehandlung-und-betriebsgrenzen). Der aktuelle Stand ergänzt sichtbaren Fortschritt und lehnt nicht unterstützte exportierte `RCLONE_*`-Optionen ab, die etwa die Inhaltsprüfung oder Dateiauswahl verändern könnten. Diese Ergänzungen sind erst nach Installation des aktuellen Pakets auf dem Server wirksam; der erfolgreiche ältere Paketlauf installiert sie nicht nachträglich. Der Abschlussmarker wird außerdem bytegenau bestätigt, einschließlich seiner Zeilenumbrüche, damit Backup und Restore dieselbe strikte Prüfung verwenden. Seit 12.09.2026 lässt sich der Hostname in Mail-Betreff und Bericht mit `MAIL_HOSTNAME` festlegen, etwa `gemini.jr-it-services.de` statt `localhost.localdomain`. Einstellung und Update-Schritte stehen unter [Mailberichte](#mailberichte). ## Automatisches Update-Paket (empfohlen) Das neue Paket übernimmt Konfigurationsabgleich, Rechteprüfung, Installation, vollständigen Backup-Probelauf und Remote-Restore-Prüfung. Du benötigst auf dem Server keinen Git-Checkout. Übertrage die gelieferte `.run`-Datei und ihre Prüfsumme per SCP und starte im Zielverzeichnis: ```bash sha256sum -c app-backup-update-VERSION.run.sha256 sudo bash app-backup-update-VERSION.run ``` VERSION durch den tatsächlichen Dateinamen ersetzen. Der längere Lauf arbeitet anschließend als systemd-Auftrag unabhängig von der SSH-Verbindung. Bei unklaren Gitea-/Cron-/rclone-Einstellungen fragt der Starter gezielt nach. Die Bedienung und der kombinierte Rückweg stehen in [UPDATE_ANLEITUNG.md](UPDATE_ANLEITUNG.md). Der Starter bestätigt nur die Übergabe. Erst `SUCCESS` in der genannten `result.txt` bestätigt Installation, Backup und Remote-Prüfung. Den Verlauf ohne Terminal-Pager anzeigen: ```bash sudo journalctl -fu app-backup-upgrade.service --no-pager ``` Ein Startabbruch nach genau 60 Sekunden in den Paketen `8e6c9a058fa9` und `56fbcdad74f4` ist durch die asynchrone Sperrübergabe behoben. Bei diesem Fehler hatte die Installation noch nicht begonnen; das neue Paket erneut verwenden. Die kurzlebige Unit kann nach einem Fehler verschwinden, das Journal bleibt abfragbar. Diagnose und Details stehen in der Update-Anleitung. Die folgende manuelle Anleitung bleibt als Alternative für individuelle Updates. Auf Rocky/RHEL starten Backup-Dienst und Update-Auftrag über Bash mit `SELinuxContext=system_u:system_r:unconfined_service_t:s0`. SELinux bleibt systemweit aktiv; **diese privilegierten Backup-Prozesse sind bewusst nicht durch SELinux eingeschränkt**. Dadurch können sie Web-/Mail-/Home-Daten samt Sicherheitsattributen sichern. Der Installer ändert keine globalen rsync-Booleans und keine Quell-Labels. Details und geprüfte Grenzen stehen in der Update-Anleitung. ## Wesentliche Änderungen - Funktionierender Restore mit Prüflauf, Entpackmodus, Komponentenauswahl und ausdrücklichem `--apply --yes`. - Fehler bei Dump, rsync, Kompression und Rückleseprüfung brechen den Lauf ab. - Wartungsmodus und Dienststopps umfassen Datenbankdump **und** Dateikopie. Danach endet die Ausfallzeit; Kompression und Upload laufen aus dem Staging. - Vorübergehende systemd-Startsperren verhindern die erneute Aktivierung konfigurierter Writer während der Kopie. Zustands- und Zeitstempelprüfungen brechen bei erkannten Neustarts vor Veröffentlichung ab. Eigene Sperren werden beim Wiederanlauf entfernt; bestehende Maskierungen bleiben erhalten. - Upload und Rückleseprüfung melden ihre Phase und standardmäßig jede Minute rclone-Statistiken im Journal (`RCLONE_STATS_INTERVAL`). Ein laufender Auftrag ist erst nach erfolgreicher Remote-Prüfung abgenommen. - Vollständige Sicherungssätze mit SHA-256-Manifest, Archivprüfung und zuletzt hochgeladenem `COMPLETE`-Marker. - Retention löscht nur vollständige neue Sicherungssätze und behält mindestens `RETENTION_KEEP_LAST` Läufe. Alte Sicherungen werden nicht automatisch gelöscht. - Private Arbeitsverzeichnisse, gemeinsame Sperre für Backup/Restore/Update und Wiederanlaufhinweise nach Abbrüchen. - Installer erhält die Serverkonfiguration und den Zeitplan und erstellt ein Rollback-Paket. ## Bestehende Installation aktualisieren **Vor dem ersten Lauf:** Für ein konsistentes Backup werden nun standardmäßig `httpd`, `php-fpm`, `postfix` und `dovecot` während der Kopie angehalten, soweit die zugehörigen Anwendungen aktiviert sind und die Dienste laufen. Fehlende Einstellungen in alten Konfigurationen erhalten diese neuen Defaults. Zusätzliche Cronjobs/Worker müssen in `QUIESCE_SERVICES` erfasst oder extern angehalten werden. Zeitfenster und Datenumfang vorher prüfen. Die bisherigen echten `*.cnf`-Dateien wurden aus dem aktuellen Repository entfernt. **Vor `git pull` auch mögliche Zugangsdaten im Server-Checkout sichern.** Historische Commits können die alten Werte weiterhin enthalten; betroffene Datenbankpasswörter wechseln. Die Dateien unter `/etc/app-backup` werden vom Installer nicht überschrieben. ```bash # Auf dem Server, im vorhandenen Checkout: cd /pfad/zum/app-backup sudo systemctl stop app-backup.timer systemctl is-active app-backup.service # Falls aktiv: den laufenden Job zuerst regulär fertig werden lassen. # Einmalige geschützte Sicherung, bevor git pull alte Dateien aus dem Checkout entfernt: sudo install -d -m 700 /var/backups/app-backup-before-v2 sudo cp -a /etc/app-backup /var/backups/app-backup-before-v2/etc-app-backup sudo cp -a etc/app-backup /var/backups/app-backup-before-v2/checkout-app-backup git status --short # Lokale Anpassungen vorher sichern/auflösen; keinen Reset erzwingen. git pull --ff-only sudo ./install.sh ``` Der Installer nennt anschließend einen Ordner wie `/var/backups/app-backup-update-XXXXXXXX` mit alten Paketdateien und `rollback.sh`. Weil der Timer zuvor gestoppt wurde, bleibt er beim Update gestoppt. Eine sonst aktive Timer-Instanz wird vom Installer nach erfolgreichem Update wieder aktiviert; deshalb ist der vorherige Stopp für die Migration wichtig. ```bash # Neue Optionen mit der bestehenden Konfiguration abgleichen (Datei enthält keine DB-Passwörter): sudo diff -u /etc/app-backup/app-backup.conf /etc/app-backup/app-backup.conf.dist sudoedit /etc/app-backup/app-backup.conf # Vorhandene Dateinamen aus WP_DB_CNF / NC_DB_CNF / GITEA_DB_CNF verwenden: sudo chown root:root /etc/app-backup/*.cnf sudo chmod 600 /etc/app-backup/*.cnf sudo chmod 600 /etc/app-backup/app-backup.conf # Den tatsächlich konfigurierten rclone-Dateipfad verwenden: sudo chown root:root /root/.config/rclone/rclone.conf sudo chmod 600 /root/.config/rclone/rclone.conf sudo /usr/local/sbin/app-backup.sh --check # Im vereinbarten Wartungsfenster den ersten vollständigen Lauf ausführen: sudo systemctl start app-backup.service sudo journalctl -u app-backup.service -n 100 --no-pager sudo cat /var/backups/app-backup/last-backup.json ``` `--check` prüft Werkzeuge, Quellpfade, Dateirechte, DB-Verbindungen, das globale DB-Recht `RELOAD` für die Dump-Sperre, bei MariaDB den Zugriff auf Routinen-Metadaten, Speicherreserve und Crypt-Zugriff. Die Prüfung liest Quellverzeichnisse über rsync und überträgt nur deren Verzeichnisattribute in einen privaten temporären Prüfbereich im Staging. Dabei werden keine Datei-Inhalte kopiert. Die DB-Rechteprüfung liest Metadaten; es werden keine Anwendungen gestoppt, keine globale DB-Sperre genommen und kein Backup oder Upload ausgeführt. Die Vorprüfung ersetzt keinen vollständigen Probelauf. Fehlt `RELOAD`, die tatsächlich verwendeten Backup-Konten ermitteln und gezielt berechtigen; siehe [DB-Rechte korrigieren](UPDATE_ANLEITUNG.md#fehlendes-db-recht-reload). In der Konfiguration insbesondere ergänzen/prüfen: ```bash RCLONE_CONFIG="/root/.config/rclone/rclone.conf" RCLONE_REMOTE_BASE="OneDriveCrypt:gemini" # vorhandenes Crypt-Ziel beibehalten! RETENTION_KEEP_LAST=3 ENABLE_REMOTE_RETENTION="false" # während der Migration zunächst aus MAIL_BACKUP_MODE="stop" # Zentrale Mailablage muss existieren; bei ausschließlich in Homes liegenden # Mailboxen ENABLE_MAIL_STORE=false und MAIL_HOME_DIRS vollständig konfigurieren. ENABLE_MAIL_STORE="true" # Tatsächliche Timer-/Service-Namen einsetzen; Timer vor seinem Service nennen: QUIESCE_SERVICES=() # Gitea war in der bisherigen mitgelieferten Konfiguration NICHT aktiviert: # ENABLE_GITEA="true" nach Prüfung aller Gitea-Pfade und DB-Einstellungen setzen. ``` Den neuen Laufnamen aus Log oder `last-backup.json` übernehmen: ```bash sudo /usr/local/sbin/app-restore.sh --remote-run appbackup_DATUM_UHRZEIT_ID --verify-only sudo /usr/local/sbin/app-restore.sh --remote-run appbackup_DATUM_UHRZEIT_ID --extract-only ``` `DATUM_UHRZEIT_ID` ist ein Platzhalter, z. B. `2026-09-10_02-30-00_a1b2c3d4`. Der Entpackmodus nennt seinen geschützten Ergebnisordner. Nach Prüfung der Dateien und einer erfolgreichen Wiederherstellungsprobe auf einem Testsystem den Zeitplan einschalten: ```bash sudo systemctl enable --now app-backup.timer systemctl list-timers app-backup.timer ``` **Rückweg bei einem Paketproblem:** Timer stoppen, aktiven Job fertig werden lassen und das vom Installer erzeugte Skript ausführen: ```bash sudo /var/backups/app-backup-update-XXXXXXXX/rollback.sh ``` Das stellt die ersetzten Paketdateien wieder her. Konfigurationsänderungen und bereits zurückgeschriebene Anwendungsdaten werden dadurch nicht rückgängig gemacht. Der Timer bleibt bis zur manuellen Prüfung gestoppt. Der alte Stand enthält die im Review beschriebenen Mängel. ## Neuinstallation Benötigt Linux mit systemd, Bash ≥ 4.4, Python ≥ 3.9, GNU tar/coreutils, rsync mit ACL/xattr-Unterstützung, zstd bzw. gzip, sudo, PHP/occ, MariaDB-Clients und rclone. Details und Rechte siehe [Dokumentation](Dokumentation.md#voraussetzungen-und-konfiguration). ```bash git clone https://gitea.jr-it-services.de/johannes/app-backup.git cd app-backup sudo ./install.sh sudoedit /etc/app-backup/app-backup.conf # DB-Dateien aus *.cnf.example erstellen; root:root und chmod 600 setzen. # rclone Crypt als root konfigurieren, Schlüssel getrennt sicher verwahren. sudo /usr/local/sbin/app-backup.sh --check ``` Der Installer aktiviert bei einer Neuinstallation den Timer standardmäßig **erst auf ausdrückliche Anforderung**. Nach Backup- und Restore-Probe: `sudo systemctl enable --now app-backup.timer`. ## Backup und Restore ```bash sudo /usr/local/sbin/app-backup.sh # Standard ist ein geprüfter Plan, ohne Änderungen an den Anwendungen: sudo /usr/local/sbin/app-restore.sh --local-run /var/backups/app-backup/archives/LAUFNAME # Ausführung überschreibt ausgewählte Dateien und Datenbankobjekte: sudo /usr/local/sbin/app-restore.sh --config /etc/app-backup/restore.conf \ --local-run /var/backups/app-backup/archives/LAUFNAME --apply --yes # Nur WordPress-Dateien; Konfiguration definiert die Zielpfade: sudo /usr/local/sbin/app-restore.sh --local-run /var/backups/app-backup/archives/LAUFNAME \ --components wordpress --no-db --apply --yes # Alte Split-Archive ohne Manifest: nur kontrolliert entpacken, manuell prüfen/importieren: sudo /usr/local/sbin/app-restore.sh --local-run /pfad/zu/einem/alten/lauf \ --legacy-unverified --extract-only ``` Ein echter Restore benötigt passende Zielverzeichnisse, Anwendungsstände und Datenbanken sowie einen DB-Account mit Wiederherstellungsrechten. `--yes` ist keine automatische Datenrettung bei Fehlern; bei einem Teilfehler bleiben angehaltene Dienste für die manuelle Prüfung stehen. ## Mailberichte Jeder beendete Backup-Lauf kann wieder einen ausführlichen UTF-8-Bericht senden: Status `SUCCESS`/`FAIL`, Host, Laufzeit, letzte Phase, Upload-/Prüfergebnis, Staging-Größen, Archivgrößen und Protokollauszug. Der Betreff verwendet wieder das Schema `[app-backup] SUCCESS/FAIL HOST LAUF`. In `/etc/app-backup/app-backup.conf` werden vorhandene Einstellungen weiterverwendet: ```bash ENABLE_MAIL_REPORT=true MAIL_TO="johannes" MAIL_FROM="" # Standard: app-backup@ MAIL_SUBJECT_PREFIX="[app-backup]" MAIL_HOSTNAME="" # Leer: System-FQDN; überschreibt nur Betreff/Bericht MAIL_INCLUDE_LOG_TAIL_LINES=200 MAIL_SEND_TIMEOUT=30 ``` Für Gemini vor dem Start des neuen Update-Pakets die aktive Konfiguration öffnen: ```bash sudoedit /etc/app-backup/app-backup.conf ``` Diese Zeile ergänzen beziehungsweise einen vorhandenen Wert ersetzen: ```bash MAIL_HOSTNAME="gemini.jr-it-services.de" ``` Anschließend das aktuelle `.run`-Paket wie oben beschrieben installieren. Es erhält die Einstellung; bereits die Mail des Abnahmebackups verwendet dann beispielsweise `[app-backup] SUCCESS gemini.jr-it-services.de appbackup_2026-…` und `Host: gemini.jr-it-services.de` im Bericht. Auch Fehlermails verwenden den Wert. Nach Installation dieser Version genügt für spätere Änderungen das Bearbeiten der Konfiguration; sie wird bei jedem Lauf neu gelesen, ein Dienstneustart ist nicht nötig. Ein leerer oder fehlender Wert verwendet weiterhin den automatisch über Python `socket.getfqdn()` ermittelten Systemnamen. `MAIL_HOSTNAME` ändert weder den Systemhostname noch Archivnamen oder Absender. Für den Absender gilt weiterhin `MAIL_FROM` beziehungsweise bei leerem Wert `app-backup@`. `MAIL_TO` darf wie bisher ein lokaler Alias oder eine Mailadresse sein. Versand über `/usr/sbin/sendmail`; die lokale Mailzustellung muss dafür eingerichtet sein. Der Bericht bleibt zusätzlich unter `/var/log/app-backup/report_LAUF.eml` mit Modus `600` erhalten. Versandfehler werden protokolliert und verändern das Backup-Ergebnis nicht. `--check` und Restore-Prüfungen senden keine Backup-Mail. Die Mail bestätigt den Backup-Lauf; beim Update ist weiterhin dessen abschließende `result.txt` nach der zusätzlichen Remote-Restore-Prüfung maßgeblich. ## Tests ```bash docker build -t app-backup-test -f tests/Dockerfile . docker run --rm --network none -v "$PWD:/src:ro" app-backup-test ``` ShellCheck, echte MariaDB-Dumps/Imports, gzip/zstd, rsync, lokale Crypt-Uploads und Remote-Restore sowie Fehlerfälle laufen im Container. systemd, Nextcloud-occ und der Mailversand sind simuliert; Nachrichten werden nur lokal erfasst. Am 12.09.2026 wurden 68 Testfälle abgedeckt: 63 bestanden im Gesamtlauf; nach Korrektur von fünf Testprüfungen für SMTP-Zeilenumbrüche bestanden alle 13 gezielt wiederholten Mail-/Installertests, einschließlich dieser fünf Fälle. ShellCheck und Shell-Syntaxprüfung bestanden. Eine echte Rocky-/Nextcloud-/Gitea-/OneDrive-Wiederherstellungsprobe und die tatsächliche Mailzustellung bleiben Teil der Serverabnahme.