# Betrieb Dieses Dokument richtet sich an die Person, die die Anlage installiert, aktualisiert und im Fehlerfall wieder zum Laufen bringt. Es beschreibt, was auf dem Host tatsächlich passiert -- nicht, wie die Oberfläche bedient wird. --- ## 1. Was wo liegt | Zweck | Pfad | |---|---| | Anwendung (Code, venv, statische Dateien) | `/srv//` | | Instanzdaten (Datenbank, Schlüssel, Lizenz) | `/srv//data/` | | Protokolle | `/var/log//` | | Umgebungsdatei | `/etc/tesm/.env` | | Zertifikate (selbstsigniert) | `/etc/tesm/certs//` | | nginx-Site | `/etc/nginx/sites-available/` | | systemd-Unit | `/etc/systemd/system/.service` | | Privilegierter Helfer | `/usr/local/lib/tesm/tesm-helper` | | Verwaltungswerkzeug | `/usr/local/bin/-admin` | `` ist `tesm` bzw. `tesm-license` -- oder, bei einer zusätzlichen Instanz, `tesm-`. Dieser Name (der **SITE_KEY**) benennt alles, was ausserhalb von `/srv/` liegt. Deshalb können zwei Installationen derselben Anwendung nebeneinander laufen, ohne sich gegenseitig die nginx-Site, die netplan-Datei oder das Sitzungscookie zu überschreiben. Alles, was ein Mensch verlieren würde, liegt unter `data/`. Das Verzeichnis wird von `install.sh` nie angefasst. --- ## 2. Installation ```bash sudo bash deploy/install.sh --app tesm sudo bash deploy/install.sh --app tesm-license ``` Wichtige Optionen: | Option | Wirkung | |---|---| | `--https` | Port 443 mit TLS, Port 80 leitet dorthin um. Ohne `--domain` entsteht ein selbstsigniertes Zertifikat. | | `--domain ` | Öffentlicher Name. Setzt `server_name`; die Site ist dann **nicht** mehr die Vorgabe-Site. | | `--acme-email ` | Erst zusammen mit `--domain` wird ein Let's-Encrypt-Zertifikat angefordert (und HSTS gesetzt). Ohne die Adresse bleibt es selbstsigniert -- interne Namen wie `lizenz.firma.local` kann keine CA bestätigen. | | `--instance ` | Zusätzliche Instanz neben der Hauptinstallation. | | `--port` / `--http-port` / `--https-port` | Abweichende Ports (nur für Testinstanzen sinnvoll). | | `--skip-packages` | Keine Systempakete installieren. | Danach: ```bash sudo -u tesm tesm-admin create-admin ``` Beim Lizenzserver zusätzlich, **in dieser Reihenfolge**: ```bash sudo -u tesm-license tesm-license-admin init-key # Signaturschlüssel -- sofort sichern! sudo -u tesm-license tesm-license-admin bootstrap # eigene Master-Lizenz sudo -u tesm-license tesm-license-admin create-admin ``` > **Der Signaturschlüssel ist nicht wiederherstellbar.** Geht > `data/master_signing_key.json` verloren, ist keine ausgestellte Lizenz mehr > prüfbar. Sichern Sie ihn getrennt vom Server. ### Zwei Anwendungen auf einem Host Beide wollen Port 80 und 443. nginx unterscheidet sie am Namen: ```bash sudo bash deploy/install.sh --app tesm --https sudo bash deploy/install.sh --app tesm-license --https --domain lizenz.firma.local ``` Die Installation **ohne** Domain wird zur Vorgabe-Site (`default_server`) und beantwortet alles, was zu keinem Namen passt. Zwei namenlose Sites auf demselben Port weist nginx ab -- richtig so, und beim Übernehmen sofort sichtbar. --- ## 3. Aktualisierung ```bash sudo bash deploy/install.sh --app tesm ``` Derselbe Befehl. Das Skript erkennt an der vorhandenen Datenbank, dass es eine Aktualisierung ist, und tut dann zusätzlich: 1. **Vollständige Sicherung** nach `/srv/-backup-` -- unabhängig davon, ob sich das Schema geändert hat. 2. Dateiabgleich; `data/` bleibt unangetastet. 3. Schema-Migrationen beim Start (versioniert, mit Prüfsumme). 4. **Gesundheitsprüfung.** Antwortet die Anwendung nicht, wird der alte Stand automatisch zurückgeholt; die fehlgeschlagene Version bleibt unter `/srv/-failed-` zur Analyse liegen. Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne `--https`. ### Ohne Quellbaum: `update.sh` `update.sh` genügt als **einzelne Datei**. Auf einem System, auf dem noch nichts von diesem Projekt liegt: ```bash curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/update.sh sudo bash update.sh tesm ``` Auf einer bestehenden Installation liegt sie schon bereit: ```bash sudo bash /srv/tesm/deploy/update.sh tesm sudo bash /srv/tesm-license/deploy/update.sh tesm-license ``` **Läuft die Fassung schon, passiert nichts.** `update.sh` vergleicht den aufgelösten Tag mit dem, was installiert ist, und endet mit `0`, bevor es eine Rückfrage stellt; `install.sh` prüft es noch einmal genauer (Anwendung **und** Kern) und bricht vor dem ersten Eingriff ab. Ein zweiter Lauf auf dieselbe Fassung baut sonst die Python-Umgebung neu, kopiert dieselben Dateien und startet den Dienst neu -- ein paar Sekunden Ausfall ohne jede Wirkung. Wer das trotzdem will (eine beschädigte Installation etwa), hängt `--force` an. Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg, eine **benannte Instanz** zu aktualisieren: ```bash sudo bash /srv/tesm/deploy/update.sh tesm ``` Ohne `--instance` entstünde daneben eine zweite Installation namens `tesm`, mit eigener Datenbank und eigener nginx-Site. Die Ports einer bestehenden Installation bleiben erhalten, auch wenn sie von den Vorgaben abweichen: eine Aktualisierung verschiebt nichts, was nicht ausdrücklich neu gesetzt wird. Ohne Tag wird `latest` **über die API aufgelöst**, nicht in die URL geschrieben: Gitea liefert unter einem Tag-Namen sonst ein automatisch erzeugtes Quell-Archiv mit HTTP 200 statt des echten Anhangs -- und das enthält kein Release, sondern den Repositoriumsstand. Die Bezugsquelle ist voreingestellt und lässt sich über die Umgebung umlenken, etwa auf eine Spiegelung: | Variable | Vorgabe | |---|---| | `TESM_RELEASE_BASE` | `https://gitea.int.eertmoed.net` | | `TESM_RELEASE_OWNER` | `alientim` | | `TESM_RELEASE_REPO` | der Anwendungsname (`tesm` bzw. `tesm-license`) | Der erwartete Anhang heisst `-.tar.gz`. ### Upgrade vom Stand 1.x (Branch `SONNET5`) Ein eigenes Skript, weil dabei mehr passiert als ein Dateiabgleich: die alte Fassung hat ein anderes Schema, andere Rechteschlüssel und ein anderes Exportformat. Auch `upgrade.sh` genügt als **einzelne Datei** -- und das ist hier der Regelfall: auf einer 1.2.8 liegt nichts von dieser Fassung. ```bash curl -fsSLO https://gitea.int.eertmoed.net/alientim/tesm/raw/branch/main/deploy/upgrade.sh sudo bash upgrade.sh --app tesm ``` Sie lädt das aktuelle Release, entpackt es und benutzt dessen `install.sh`. Liegt das Skript in einem entpackten Release, wird dieses benutzt statt eines Downloads -- wer ein Paket ausgepackt hat, will nicht, dass ungefragt ein anderes geladen wird: ```bash sudo bash deploy/upgrade.sh --app tesm sudo bash deploy/upgrade.sh --app tesm-license ``` Eine bestimmte Version statt `latest`: `--tag v2.0.8`. **Wenn etwas schiefgeht.** Schlägt `install.sh` fehl, dreht `upgrade.sh` den Lauf selbst zurück: der neue Dienst wird gestoppt, das Verzeichnis aus der Sicherung wiederhergestellt und der alte Dienst wieder eingeschaltet. Das Verzeichnis allein genügt dafür nicht -- bei einem Upgrade in dasselbe Verzeichnis schreibt `install.sh` auch systemd-Unit, nginx-Site, logrotate und Banner neu und ersetzt das venv. Die Sicherung enthält deshalb alles davon, und ein Manifest hält fest, woher der Stand kam. Später von Hand zurückdrehen, etwa nach einem Lauf, der durchlief aber nicht gefallen hat: ```bash sudo bash upgrade.sh --app tesm --rollback # neueste Sicherung sudo bash upgrade.sh --app tesm --rollback /srv/tesm-upgrade-20260908-225428 ``` Die Sicherung bleibt danach liegen; gelöscht wird nichts. Ohne `--source` wird die alte Fassung an zwei Stellen gesucht: unter `/srv/` und unter `/srv/-sonnet5`. Der zweite Ort ist der, an den `rename.sh` sie verschiebt, sobald der Hauptname für die neue Installation gebraucht wird -- danach liegt unter `/srv/` die **laufende** Installation. Erkannt wird eine 1.x an der `sqlite.db` im Wurzelverzeichnis; die neue Fassung legt ihre Datenbank unter `data/app.db` ab. Der Ablauf ist die Vorgabe und braucht keine Schalter: | | | |---|---| | 1. Sichern | Vollständige Kopie der alten Installation nach `/srv/-upgrade-/alt` (ohne `venv`, Verzeichnis 0700) | | 2. Abschalten | Alter Dienst **und seine Timer** werden gestoppt und deaktiviert | | 3. Installieren | `install.sh` dieses Verzeichnisses | | 4. Exportieren | Aus der Kopie entsteht `export-alt.json` im **alten** Format | | 5. Übersetzen | Daraus `import-neu.json` im neuen Format | | 6. Einspielen | Diese Datei geht durch denselben Import wie eine Sicherung | Warum die Sicherung **vor** dem Export steht: ohne `--instance` installiert `install.sh` in dasselbe Verzeichnis und ersetzt die alte Installation. Wer erst exportiert und dann sichert, sichert im schlechtesten Fall nichts mehr. Gelesen wird ausschliesslich aus der Kopie -- die alte Installation wird nie verändert. Die beiden Zwischendateien bleiben liegen; ihre Passphrase steht daneben in `passphrase.txt` (0600, Verzeichnis 0700). `export-alt.json` ist ein echter Export des Vorgängers und taugt deshalb auch als Rückweg. **Daneben statt darüber.** Mit `--instance` entsteht eine zweite Installation, und die alte bleibt vollständig liegen: ```bash sudo bash deploy/upgrade.sh --app tesm --instance test --keep-old-running --port 5100 --http-port 8080 ``` **Was nicht mitkommt** -- und warum: * **Lokale Konten.** Der alte Hash ist bcrypt, hier gilt Argon2id. Ein Hash ist nicht umrechenbar; das ist sein Zweck. Erstes Konto mit `tesm-admin create-admin`. * **Gruppen und Rechte.** Die Rechteschlüssel wurden neu geschnitten. * **LDAP, Webserver, Zertifikat.** Von Hand nachziehen. Beim **Lizenzserver** gibt es Schritt 4 und 5 nicht: die Übernahme liest die alte Datenbank unmittelbar, holt Endpunkt und Anbieterdaten aus ihren Einstellungen und **signiert jede Lizenz neu**. Der alte Signaturschlüssel wird dabei übernommen -- sonst müsste jeder Kunde einen Ausstellerwechsel bestätigen; `--new-key` erzwingt das Gegenteil. Der Bericht nennt namentlich, wer eine neue Lizenzdatei braucht, denn die Bindung an eine Maschine gilt nur für die alte Datei. Nur die Anwendung, ohne Daten: `--no-data`. Nur zeigen, was geschähe: `--dry-run`. --- ## 4. HTTPS Der Zielzustand ist immer: **Port 443 mit TLS, Port 80 leitet um, ausser dem ACME-Pfad.** Drei Wege dorthin: **a) Bei der Installation** -- siehe oben, `--https`. **b) Nachträglich über die Kommandozeile:** ```bash sudo -u tesm tesm-admin web-setup --https --self-signed --apply sudo -u tesm tesm-admin web-setup --https --domain tesm.firma.de \ --acme --acme-email it@firma.de --hsts --apply ``` `--print-config` zeigt die Datei, ohne etwas zu schreiben. **c) In der Oberfläche** unter *Einstellungen -> Webserver und TLS*. Danach muss `COOKIE_SECURE` in `/etc/tesm/.env` auf `1` stehen (alle drei Wege erledigen das selbst) und der Dienst neu gestartet werden. ### Die Site-Datei wird immer neu erzeugt Sie ist **kein** Ort für Handarbeit. Jede Übernahme schreibt sie vollständig aus der Vorlage neu. Was früher von Hand hineingeschrieben wurde -- Domain, Ports, Zertifikatspfade, HSTS, Upload-Grenze -- ist heute Einstellung in der Anwendung. Der Grund steht im Vorgängerprojekt: dort wurde die Datei nur angelegt, wenn sie fehlte. Ein einmal falscher `alias`-Pfad überlebte dadurch jahrelang jedes Update, und die Oberfläche kam die ganze Zeit ohne Design. ### Selbstsigniert oder Let's Encrypt? * **Öffentlich erreichbar, echter DNS-Name:** Let's Encrypt. Nur dann ist HSTS sinnvoll. * **Geschlossenes Netz:** selbstsigniert. Browser zeigen eine Warnung, die Verbindung ist trotzdem verschlüsselt und das Sitzungscookie darf `Secure` tragen. **HSTS hier nicht einschalten** -- der Browser merkt sich die Vorgabe monatelang und die Warnung lässt sich dann nicht mehr wegklicken. Die Erneuerung von Let's Encrypt läuft über `certbot certonly --webroot` (bewusst nicht über das nginx-Plugin, das die erzeugte Site umschreiben würde). Der ACME-Pfad bleibt auch bei aktiver Umleitung erreichbar. --- ## 4a. Anmeldebanner (MOTD) `install.sh` legt `/etc/update-motd.d/50-` an. Bei jeder SSH-Anmeldung steht dann der Zustand der Installation im Login-Banner: ``` TESM · TimEShepManager UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · BETRIEBSSYSTEM GEBEN (PXE) 3 Clients online · 0 nicht erreichbar · DHCP: 2 Leases aktiv ``` Das Skript liegt unter `/usr/local/lib/tesm/tesm-motd` und benutzt **nur die Standardbibliothek**: kein venv, kein Import der Anwendung, die Datenbank wird nur lesend geöffnet. Ein Lauf dauert Millisekunden. Der Vorgänger importierte dafür die ganze Flask-Anwendung -- bei jeder Anmeldung, und nach einem Wechsel des Quellverzeichnisses zeigte er stillschweigend die Zahlen einer Datenbank, die niemand mehr füllte. Von Hand prüfen: ```bash sudo /usr/local/lib/tesm/tesm-motd --app tesm --root /srv/tesm-test ``` Der Aufruf im Banner ist mit `timeout 3` und `|| true` abgesichert: ein hängender oder fehlschlagender Bannerlauf darf eine Anmeldung nie blockieren. Ein Banner der Vorgängerfassung (`50-tesm`, das `motd_banner.py` aufruft) wird entfernt -- nur dieses, nichts anderes in dem Verzeichnis. --- ## 4b. Installation umbenennen Aus einer Instanz die Hauptinstallation machen (oder umgekehrt): ```bash sudo bash deploy/rename.sh --app tesm --from tesm-test --to tesm sudo bash deploy/rename.sh --app tesm-license --from tesm-license-test --to tesm-license ``` Der Name steckt an mehr Stellen als im Verzeichnisnamen, und drei davon sind leicht zu übersehen: * **`SITE_KEY`** in der Umgebungsdatei -- daraus entsteht der Name des Sitzungscookies. Bleibt er alt, heisst das Cookie weiter wie die alte Instanz. * **`web_cert_path`, `web_key_path`, `web_acme_webroot`** in der Datenbank. Sie enthalten den Namen; ein Zertifikat unter dem alten Pfad findet nginx nach dem Umzug nicht mehr. * **`server_name`** der nginx-Site. `install.sh` schreibt die Datei aus seiner Vorlage neu, sobald sich die `alias`-Pfade ändern -- und die ändern sich hier zwangsläufig. Ohne den Namen aus der Datenbank stünden danach zwei Sites mit `server_name _` auf Port 80, und die erste gewinnt. Ausserdem wird das **venv verworfen und neu gebaut**: seine Konsolenskripte tragen den absoluten Pfad im Shebang, und nach dem Umzug zeigt `venv/bin/gunicorn` auf einen Interpreter, den es nicht mehr gibt. systemd meldet dann `No such file or directory` -- für eine Datei, die sichtbar da ist. Belegt der Zielname noch eine Installation der Fassung 1.x, wandert **alles**, was ihn belegt, in ein Archiv `/srv/-sonnet5`: das Verzeichnis, die Protokolle unter `/var/log/`, der ACME-Pfad, die logrotate-Datei, und die systemd-Units samt nginx-Site unter `systemd-alt/`. Gelöscht wird nichts. Ports und HTTPS bleiben, wie sie waren; ein vorhandenes Zertifikat zieht mit und wird weiterverwendet. **Alle angemeldeten Benutzer werden abgemeldet** -- das Sitzungscookie heisst danach anders. Danach ist auch das Aktualisieren einfacher, weil der Instanzname wegfällt: ```bash sudo bash /srv/tesm/deploy/update.sh tesm ``` --- ## 4c. Installation entfernen Das Gegenstück zu `install.sh`: ```bash sudo bash /srv/tesm/deploy/uninstall.sh --app tesm --dry-run sudo bash /srv/tesm/deploy/uninstall.sh --app tesm sudo bash /srv/tesm/deploy/uninstall.sh --app tesm-license --instance test --yes ``` Entfernt werden Dienste (`.service`, `-monitor`, `-cleanup` samt Timer), die nginx-Site, der Begrüßungshaken, der Shim unter `/usr/local/bin`, die Umgebungsdatei, das Zertifikatsverzeichnis, das Protokollverzeichnis und die Installation selbst. **Die Daten bleiben.** Datenbank, Schlüssel und Lizenz gehen vorher in ein Archiv (`/srv/-daten-.tar.gz`, nur root). Sie sind das einzige an einer Installation, was sich nicht wiederherstellen lässt. Wer sie wirklich los sein will, nimmt `--purge` — das löscht zusätzlich die Rückwege älterer Aktualisierungen (`/srv/-backup-*`). **Geteiltes bleibt, solange es jemand braucht.** Die sudoers-Datei und der Dienstbenutzer tragen den Namen der *Anwendung*, nicht den der Instanz; der Helfer unter `/usr/local/lib/tesm` gehört allen Installationen beider Anwendungen. Sie verschwinden erst mit der letzten. Sonst legte der Rückbau einer Testinstanz die Produktion daneben lahm. `--dry-run` zeigt den Plan, ohne etwas zu ändern. Ein zweiter Lauf auf eine bereits entfernte Installation sagt das und endet mit 0. Warum es das gibt: wer eine Installation von Hand löscht (`rm -rf /srv/…`), lässt Unit, Begrüßungshaken, Shim, sudoers-Datei und nginx-Site zurück. systemd versucht die Unit danach in einer Dauerschleife zu starten, und der Haken druckt bei jeder Anmeldung seinen Banner. Auf dem Testserver standen so neun tote Dienste und vierzehn Banner. --- ## 5. Dienste ```bash systemctl status tesm.service # Weboberfläche (gunicorn) systemctl status tesm-monitor.service # Überwachungsschleife (nur TESM) systemctl status tesm-license.service journalctl -u tesm -n 100 -f ``` Die Units laufen unprivilegiert (`User=tesm`) mit `NoNewPrivileges`, `ProtectSystem=strict`, `PrivateTmp` und einer engen `RestrictAddressFamilies`-Liste. Schreibrechte gibt es nur auf `/srv//data` und `/var/log/`. --- ## 6. Root-Rechte Die Anwendung hat keine. Alles, was Root braucht, geht durch **einen** Pfad: ``` /usr/local/lib/tesm/tesm-helper [argumente] ``` Der Helfer gehört `root`, ist per sudoers ausschliesslich für genau diesen Pfad mit `NOPASSWD` freigegeben und prüft jedes Argument gegen eine Positivliste. Er reicht nie etwas an eine Shell weiter (kein `eval`, kein `sh -c`). Verben ansehen: ```bash sudo /usr/local/lib/tesm/tesm-helper grep -n ')$' /usr/local/lib/tesm/tesm-helper ``` Regel für Änderungen: **Jedes neue Verb braucht eine Argumentprüfung.** `tests/test_repo_hygiene.py` erzwingt, dass jedes der Anwendung bekannte Verb im Helfer vorkommt und dass jede Pfadprüfung `..` abweist. --- ## 7. Sicherung und Wiederherstellung In der Oberfläche unter *Sicherung* -- verschlüsselte Ausfuhr (Argon2id + AES-256-GCM) mit einem selbst gewählten Kennwort. Der Import zeigt zuerst eine Vorschau, bevor irgendetwas geschrieben wird. Auf dem Host reicht: ```bash sudo systemctl stop tesm sudo tar -czf /root/tesm-data-$(date +%F).tar.gz -C /srv/tesm data sudo systemctl start tesm ``` Unverzichtbar: * `data/app.db` -- alle Daten * `data/secret.key` -- ohne sie sind alle Sitzungen ungültig * `data/data.keys` -- ohne sie sind **alle gespeicherten Geheimnisse verloren** * `data/license.json` und `data/license_key.json` * Lizenzserver zusätzlich: `data/master_signing_key.json` --- ## 8. Wenn etwas nicht geht **Die Anwendung antwortet nicht.** ```bash journalctl -u tesm -n 200 --no-pager curl -s http://127.0.0.1:5000/health ``` Antwortet gunicorn, aber der Browser nicht, liegt es an nginx. **nginx lässt sich nicht neu laden.** ```bash sudo nginx -t ``` Die Übernahme aus der Anwendung nimmt eine abgelehnte Konfiguration selbst zurück. Eine von Hand bearbeitete Datei tut das nicht -- in dem Fall hilft `install.sh`, das eine unbrauchbare Site durch die Vorlage ersetzt. **Anmeldung schlägt ohne Fehlermeldung fehl.** Fast immer `COOKIE_SECURE=1` ohne HTTPS. Der Browser bekommt dann ein Cookie, das er über HTTP nie zurücksendet. ```bash grep COOKIE_SECURE /etc/tesm/tesm.env ``` **"Die Anfrage kam von einer fremden Herkunft (Origin/Referer)."** Der Reverse Proxy reicht den Host falsch durch. In der Site-Datei muss `proxy_set_header Host $http_host;` stehen -- **nicht** `$host`, das verwirft den Port. **Statische Dateien fehlen, die Oberfläche ist unformatiert.** ```bash grep alias /etc/nginx/sites-available/tesm ls /srv/tesm/static /srv/tesm/static-core ``` Beide `alias`-Pfade müssen in das Installationsverzeichnis zeigen. **Zwei Instanzen stören sich.** `SITE_KEY` in beiden `/etc/tesm/*.env` vergleichen -- er muss sich unterscheiden. Sonst teilen sich beide nginx-Site und Sitzungscookie. **Die Wartung meldet "Der SSH-Host-Schlüssel ist unbekannt".** Kein Fehler, sondern die Absicht: ein Hintergrundauftrag darf keinen fremden Schlüssel akzeptieren. Die Freigabe erfolgt von Hand, in zwei Schritten, auf der Detailseite des Geräts (*Clients -> Gerät -> SSH-Host-Schlüssel*): erst **Auslesen**, dann den Fingerprint mit einer unabhängigen Quelle vergleichen, dann **Freigeben**. Die Wartungsübersicht zeigt in einer eigenen Spalte, für welche Geräte das noch offen ist. Für Switche liegt dieselbe Freigabe auf der Switch-Detailseite. Der Schlüssel wird nach **IP und Port** gespeichert, nicht nach Name -- TESM verbindet sich immer über die hinterlegte IP-Adresse. Ändert sich die IP eines Geräts, ist der Schlüssel erneut freizugeben. **Die Prüfkette des Änderungsprotokolls ist gebrochen.** Unter *Diagnose* sichtbar. Das Protokoll ist verkettet gehasht; ein Bruch heisst, dass jemand direkt in die Datenbank geschrieben hat. Der Eintrag, ab dem es nicht mehr stimmt, wird genannt. --- ## 9. Zurück auf eine ältere Version ```bash sudo systemctl stop tesm sudo mv /srv/tesm /srv/tesm-kaputt sudo cp -a /srv/tesm-backup- /srv/tesm sudo systemctl start tesm ``` Schema-Migrationen laufen nur vorwärts. Ein Rücksprung über eine Schema-Änderung hinweg braucht deshalb auch die Datenbank aus der Sicherung -- genau das liegt im Backup-Verzeichnis.