Ctrl K
ComunidadGuías…

deploy/README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Auslieferung

Ein Image, ein Container, ein Volume (`/app/data`) — [docs/00-DECISIONS.md D-10](../docs/00-DECISIONS.md#d-10-auslieferung).
Multi-Arch: `linux/amd64` (x86-Rechner) und `linux/arm64` (Raspberry Pi).

Wer nur Docker hat, beginnt mit „Installation ohne das Repo“. Die Abschnitte „Bauen“, „Starten
aus dem Repo“ und „Deploy auf einen eigenen Pi per SSH“ richten sich an alle, die Nodvard Deck
aus dem Quellcode bauen.

## Installation ohne das Repo

Für Nutzer, die nur Docker haben: Die Datei [`compose.standalone.yml`](compose.standalone.yml)
als `compose.yml` in einen leeren Ordner laden und dort ausführen. Sie zieht das fertige Image
`ghcr.io/nodvard/deck:latest` (Multi-Arch: `linux/amd64` und `linux/arm64`), baut nichts und braucht
kein Repository:

```bash
mkdir nodvard-deck && cd nodvard-deck
curl -fsSL -o compose.yml https://raw.githubusercontent.com/nodvard/deck/main/deploy/compose.standalone.yml
docker compose up -d
# dann im Browser http://<Adresse-des-Rechners>:8080 öffnen
```

Port ändern: Umgebungsvariable `NODVARD_DECK_PORT` (sonst 8080; der alte Name `LATTICE_PORT` gilt nur noch in
dieser Version). Die Daten liegen im Volume `nodvard-deck-data`; ein eingebundener Ordner (`./daten:/app/data`) geht auch, weil der Entrypoint die
Besitzrechte selbst setzt (siehe unten). Das Image entsteht bei jedem Tag `v*` durch
`.github/workflows/release.yml` (Tags `:<version>`, `:<major>.<minor>`, `:latest`).

**Benutzerwechsel im Container:** Das Image startet als root. `entrypoint.sh` setzt dann die Besitzrechte an
`/app/data` (nur dort, ohne Links zu folgen, nur das, was nicht schon `lattice` gehört) und startet sich
per `setpriv` als Benutzer `lattice` (UID 1000) neu – Migration und Anwendung laufen nie als root. Läuft der
Container schon als Nicht-root (`user: "1000:1000"`, Kubernetes), ändert sich nichts; dann muss der Ordner
diesem Benutzer gehören. `docker compose exec` läuft als root; `python -m nodvard_deck.admin` wechselt selbst zum
Besitzer des Datenordners. Hilfscontainer mit `--entrypoint` (`backup.sh`, `restore.sh`) laufen mit
`--user 1000:1000`.

**Dateirechte:** Was `entrypoint.sh` startet (Migration, Anwendung, Notseite), legt neue Dateien und Ordner nur für
den Besitzer an (`umask 077`); `python -m nodvard_deck.admin` und `python -m nodvard_deck.boot` tun das auch ohne den
Entrypoint (`docker compose exec`). Bei jedem Start setzt `entrypoint.sh`, nie als root, `/app/data` auf 0700 und
nimmt Gruppe und anderen alle Rechte an den Dateien und Ordnern darin (Links bleiben unberührt); so werden auch
ältere Installationen mit offenen Rechten (0644/0755) nachgezogen. Ein eingebundener Hostordner ist danach nur für
UID 1000 und root lesbar: Backup-Werkzeuge oder Dateimanager, die als anderer Benutzer laufen, brauchen `sudo`.
Unterstützt der Speicher keine Rechte (manche NAS-Freigaben), läuft der Start weiter, im Protokoll steht höchstens
eine Warnung. Code, Erweiterungen und Oberfläche unter `/app` gehören root und sind für `lattice` nur lesbar; unter
`/app` darf `lattice` nur in `/app/data` schreiben. So kann ein Prozess als `lattice` in den Code unter `/app` nichts
einschleusen, das später als root startet (Healthcheck, `docker compose exec`).

**Zusatzgruppen (`group_add:`):** Durch den Benutzerwechsel (`--clear-groups`) gelten sie nicht mehr. Wer sie braucht
(z. B. eine Gruppe, die einen NAS-Ordner freigibt), startet den Container selbst als Nicht-root und setzt beides
zusammen: `user: "1000:1000"` und `group_add: [...]`; der Datenordner muss dann diesem Benutzer gehören.

## Bauen

```bash
# Aus dem Repository-Root:
docker buildx build --pull -f deploy/Dockerfile \
  --platform linux/amd64,linux/arm64 \
  -t nodvard-deck:latest \
  --load .    # --load fuer eine einzelne lokale Architektur; fuer beide zugleich
              # braucht es einen Push in eine Registry (--push statt --load) --
              # `docker buildx` kann kein Multi-Arch-Manifest lokal "laden".
```

Für einen einzelnen Zielrechner reicht ein normaler Build ohne `buildx`:

```bash
docker build --pull -f deploy/Dockerfile -t nodvard-deck:latest .
```

`--pull` holt die Basis-Images (`python`, `node`) jedes Mal frisch. Ohne bleibt die einmal geladene Fassung im
Build-Cache liegen, und Sicherheitskorrekturen darin kommen nie im Image an.

Nur einige Module ins Image nehmen (etwa für ein Einzelprodukt), durch Kommas getrennt:

```bash
docker build --pull -f deploy/Dockerfile --build-arg NODVARD_DECK_EXTENSIONS=docker -t nodvard-docker:test .
```

Ohne Angabe kommen alle Module mit, wie bisher. Ein unbekannter Name oder eine Liste ohne Modul bricht den Build ab.
Tesseract (Texterkennung für Dokumente) kommt nur mit, wenn das Modul `documents` dabei ist; die Python-Pakete des Kerns
bleiben in jedem Image dieselben. Braucht ein Modul ein anderes (etwa `npm` das Modul `docker`), nimm beide in die Liste,
sonst fehlt ihm im Image der Partner. Ein Image mit weniger Modulen gehört zu einem eigenen Datenvolume: Mit den Daten
eines Images, in dem mehr Module waren, startet es nicht und zeigt nur die Notseite.

Laufzeitdaten gelangen nicht ins Image: `.dockerignore` schließt in jedem Unterordner (z. B. ein `backend/data/` nach
einem Start aus `backend/`) Datenordner `data/`, Datenbanken (`*.db`, `*.sqlite`), Sicherungen (`*.ndbak`), Schlüssel
(`*.key`, `jwt_secret*`, `vault_keyring.json`) und `.env`-Dateien aus.

## Starten aus dem Repo

```bash
cd deploy
docker compose up -d
```

Danach `http://<host>:8080/` öffnen. Eine frische Installation (kein Nutzer in der
Datenbank) zeigt automatisch den **Einrichtungsassistenten** (`/setup` statt `/login`,
solange `GET /api/v1/auth/bootstrap` `{"needed": true}` liefert): Administrator-Konto,
Zeitzone, Auswahl der Module, optional Zwei-Faktor-Anmeldung und Aussehen (Produktname,
Untertitel, Akzentfarbe). Logo, alle fünf Farben und Support-Link stellt man danach unter
Einstellungen → Aussehen ein (`PUT /api/v1/branding`). Schritt für Schritt:
[docs/11-ERST-EINRICHTUNG.md](../docs/11-ERST-EINRICHTUNG.md).

**Log-Rotation:** Die Docker-Logdatei des Dashboards ist auf 3 Dateien à 10 MB begrenzt
(`logging:` in beiden mitgelieferten Compose-Dateien), damit sie nicht ungebremst wächst. Die
Einstellung gilt, sobald der Container mit `docker compose up -d` neu erstellt wird.

## Deploy auf einen eigenen Pi per SSH

Für alle, die aus dem Quellcode bauen und das Ergebnis auf einen eigenen Raspberry Pi (oder
einen anderen Linux-Rechner mit Docker) bringen wollen. Ein Docker-Build **auf dem Pi** ist
langsam und belastet ihn stark (hohe Last, Swap, verzögerter Containerstart, dadurch auch
falsche „DEPLOY-FAIL“-Meldungen). `scripts/deploy_pi.sh` baut deshalb auf dem PC und kopiert
nur das fertige Image:

```bash
# Aus dem Repository-Root in Git Bash, Docker Desktop läuft (QEMU für arm64 ist dabei):
scripts/deploy_pi.sh
```

Was das Skript tut:

1. `docker buildx build --pull --platform linux/arm64 -f deploy/Dockerfile --build-arg APT_REFRESH="$(date +%G-W%V)" -t nodvard-deck:pi-<sha> --load .`
   (`--pull`: Basis-Images frisch holen, siehe „Bauen“; `APT_REFRESH`: die Debian-Pakete wie Tesseract einmal je
   Kalenderwoche frisch holen statt aus dem Cache, mit den Sicherheitsupdates seit dem letzten Bau).
   Die Frontend-Stufe läuft dank `FROM --platform=$BUILDPLATFORM` nativ, nur die
   Python-Stufe wird für arm64 gebaut (alle Wheels vorhanden, ~2 Min beim ersten Mal).
2. `deploy/` (compose, entrypoint) per `git archive` auf den Pi kopieren.
3. `docker save | gzip | ssh 'gunzip | docker load'`.
4. Vorab (`pi_switch.sh precheck`, bis zu 5 Versuche im Abstand von 3 s): Ist der Dienst schon
   eingerichtet (`GET /api/v1/auth/bootstrap` → `"needed":false`)? Antwortet das alte Image mit 404,
   weil es den Endpunkt nicht kennt, gilt er als eingerichtet, sofern der Container das Volume
   `deploy_lattice_data` hat. **Läuft ein Dashboard-Container, antwortet aber gar nicht, bricht das
   Skript ab, bevor etwas verändert wird** (erst klären: `docker ps`, `docker logs`).
   Danach merkt sich das Skript (`pi_switch.sh loaded`), welche Erweiterungen der laufende Dienst geladen hat. Jeder Start
   und jedes Ein- oder Ausschalten einer Erweiterung schreibt die Zeile `Erweiterungen geladen: <Kennungen>` ins Protokoll:
   Speicher-Kennungen (nach einer Umbenennung die alte, z. B. `nexus-soc` für Nodvard Shield), sortiert, durch Komma
   getrennt, `(keine)`, wenn nichts läuft. Gelesen wird die letzte solche Zeile im ganzen Docker-Protokoll des laufenden
   Containers. Kennt der alte Stand die Zeile noch nicht (erster Lauf mit dieser Prüfung) oder ist sie aus dem Protokoll
   herausgerollt (Docker behält 3 × 10 MB), gibt es nur einen Hinweis und keinen Vergleich. Vorher prüfen:
   `docker logs deploy-nodvard-deck-1 2>&1 | grep 'Erweiterungen geladen' | tail -n 1`; fehlt die Zeile, erst
   `docker restart deploy-nodvard-deck-1`.
5. Auf dem Pi läuft `deploy/pi_switch.sh switch <tag>` (die Schritte stehen dort, getestet in
   `backend/tests/test_deploy_switch.py`), **von der ssh-Leitung abgekoppelt** (`setsid`, Ausgabe in
   `pi_switch.out`/`pi_switch.err` im Deploy-Ordner): Reißt die Verbindung ab, läuft der Schritt auf
   dem Pi zu Ende. Das Skript meldet dann `DEPLOY-FAIL … abgerissen` **ohne** automatischen
   Rollback; bitte auf dem Pi nachsehen (`docker ps -a`, `cat pi_switch.out`, `bash pi_switch.sh verify`):
   - **vorab prüfen, ohne etwas zu ändern** (sonst Abbruch mit Exit 3, `DEPLOY-FAIL`, kein
     Rollback nötig): Docker erreichbar (`docker info`), das Image vorhanden,
     `docker compose -p deploy config -q` läuft durch (zu alte Compose-Version fällt hier auf), und es gibt keinen
     Container `deploy-lattice-1` mehr (siehe [Stand von vor 0.6](#stand-von-vor-06));
   - das bisherige `nodvard-deck:latest` als `nodvard-deck:previous` sichern;
   - das neue Image als `nodvard-deck:latest` taggen;
   - `docker compose -p deploy up -d --no-build` (Projekt immer fest `deploy`; kein
     `--remove-orphans`, das wirkt auf das ganze Projekt);
   - prüfen (`verify`): `deploy-nodvard-deck-1` hat den Status `running` (keine
     Neustart-Schleife), trägt das Image von `nodvard-deck:latest` und hat unter `/app/data` das
     Volume `deploy_lattice_data` (ein anderes wäre leer).
6. Bis zu 4 Minuten auf `"status":"ok"` von `/api/v1/health` warten, dann `verify` noch einmal
   („gesund“ allein beweist nicht, dass das neue Image läuft), dann den **Datencheck**: War der Dienst
   vorher eingerichtet, muss `/api/v1/auth/bootstrap` weiter `"needed":false` liefern (sonst sind
   die Daten weg). Dann der **Erweiterungen-Vergleich**: Jede Erweiterung, die vorher geladen war, muss im neuen
   Container wieder geladen sein (neue dazu sind in Ordnung). Das Skript wartet dafür bis zu 30 Sekunden auf die Zeile des
   neuen Stands (`LOADED_WAIT_S`, nie über den Rest der Health-Wartezeit hinaus). Fehlt eine Erweiterung oder fehlt die
   Zeile ganz, obwohl der alte Stand eine hatte, gilt das wie ein gescheiterter Datencheck. Die Ausgabe nennt die
   fehlenden Kennungen. Soll eine davon mit dem neuen Stand bewusst wegfallen: sie erst in den Einstellungen
   ausschalten, dann das Deploy wiederholen. Dann die Logs der letzten 5 Minuten auf `Traceback` prüfen. Scheitert
   diese Log-Prüfung selbst (Verbindung weg), ist der Zustand unklar: `DEPLOY-FAIL`, ebenfalls ohne automatischen
   Rollback.
7. Scheitert einer der Schritte, wird **automatisch zurückgeschaltet** (`pi_switch.sh rollback`):
   `nodvard-deck:previous` wird wieder `nodvard-deck:latest` und läuft per Compose. Ist die neue Compose-Datei selbst
   falsch (etwa ein anderes Volume), gilt sie auch für den Rückweg: Dann steht `ROLLBACK-FAIL` da. Danach wartet das Skript, bis `/api/v1/health` wieder antwortet. Klappt das nicht, steht
   **`ROLLBACK-FAIL`** in der Ausgabe (Dienst vermutlich aus), sonst `DEPLOY-FAIL: Rollback
   ausgeführt`. Gibt es nichts zum Zurückschalten (frische Maschine), steht das ehrlich dort.

   **Ausnahme: kein automatischer Rollback nach einer Migration.** Hat der neue Container die
   Datenbank umgebaut und ist gestartet (`.boot/state.json`: `last_migration.at` nicht älter als der
   Container, `started_ok: true`), lehnt das alte Image die Daten ab und zeigt nur die Notseite. Meldet danach eine Prüfung ein Problem (Datencheck, Erweiterungen, Tracebacks, Absturz oder
   Neustart-Schleife nach dem Health-Ok), endet das Skript mit `DEPLOY-FAIL … hat die Datenbank aber
   schon umgebaut … KEIN automatischer Rollback`. Lässt sich der Migrationsstand nicht feststellen
   (Datei nicht lesbar, Verbindung weg), heißt es `Zustand unklar, KEIN automatischer Rollback`.
   Dann zuerst `docker ps -a` und `docker logs --since 10m deploy-nodvard-deck-1` ansehen. Läuft
   die neue Version nicht brauchbar und soll der alte Stand zurück, bewusst
   `bash pi_switch.sh rollback` ausführen und auf der Notseite (Port 8080) den Stand von vor dem
   Update wiederherstellen. Den Notfallcode dafür zeigt `docker logs deploy-nodvard-deck-1`.
   Änderungen seit dem Update gehen dabei verloren. Das klappt nur, wenn die alte Version
   (`nodvard-deck:previous`) die Notseite schon kennt. Sonst die neue Version laufen lassen und den
   Fehler dort beheben. Bei falschem Volume
   oder falschem Image versucht das Skript weiter den automatischen Rückweg; die alten Daten bleiben dabei unberührt.
8. Erst wenn alles gestimmt hat, räumt `pi_switch.sh cleanup` auf: alte `nodvard-deck:pi-*`-Tags (außer den Ständen
   hinter `latest`/`previous`). Danach `docker image prune -f`. Das Volume wird nie angefasst. Images von vor 0.6
   (`lattice:*`) lässt es liegen; die lassen sich mit `docker rmi` von Hand löschen.

Solange noch nicht aufgeräumt ist (oder wenn Docker/ssh mittendrin abbricht), liegen alte `nodvard-deck:pi-*`-Images
auf dem Pi (ein paar hundert MB je Stand). Sie verschwinden mit dem nächsten erfolgreichen Deploy oder per
`bash pi_switch.sh cleanup`.

**Was bewusst gleich bleibt:** Das Volume heißt weiter `deploy_lattice_data` (in der
Compose-Datei fest mit `name: deploy_lattice_data` eingetragen, Compose-Projekt fest `name: deploy`,
Schlüssel `lattice_data`; alle Skripte rufen `docker compose -p deploy` auf), die Daten werden
nicht kopiert. Der Zielordner auf dem Zielrechner ist `~/<DEPLOY_ROOT>`; der Standardwert
von `DEPLOY_ROOT` ist `lattice-deploy-test` (ein Name aus der Zeit vor der Umbenennung, nur
ein Vorgabewert, frei wählbar). Dort liegt auch die `.env`. Den Dienst nicht mit
`container_name:`/`hostname:` festnageln: das Dashboard erkennt sich an Hostname = Container-ID.

<a id="stand-von-vor-06"></a>**Stand von vor 0.6.** Vor Version 0.6 hieß der Container `deploy-lattice-1` (Image `lattice:latest`). Den Umstieg davon
kann seit 0.12 nur noch Version 0.11 ausliefern. `scripts/deploy_pi.sh` prüft das, bevor es etwas auf den Pi überträgt;
`pi_switch.sh`, `backup.sh` und `restore.sh` brechen ebenso ab, solange dieser Container läuft oder `nodvard-deck:latest`
fehlt, und ändern nichts. Der Weg: erst von Hand sichern (Befehl unter [Backup / Restore](#backup--restore)), dann mit
dem Stand von 0.11 ausliefern (`git checkout v0.11.1`, `scripts/deploy_pi.sh`), danach zurück auf `main` und diese
Version ausliefern. Liegt nur noch ein gestoppter `deploy-lattice-1` neben `nodvard-deck:latest` (der Umstieg ist schon
gelungen), stört er beim Sichern und Einspielen nicht; vor dem nächsten Deploy reicht `docker rm deploy-lattice-1`.

**Migration und Rückweg.** Der Rollback setzt nur das Image zurück, Migrationen bleiben. Ein Stand mit neuer Migration
lässt sich daher nicht mehr auf das alte Image zurückschalten; vorher `deploy/backup.sh` laufen lassen.

Vorher gehört die komplette Test-Suite grün (siehe [README.md, Entwicklung](../README.md#entwicklung), ~6 Min) – das Skript
prüft das bewusst nicht selbst. `scripts/deploy_pi.sh --no-build` liefert das zuletzt
gebaute Image erneut aus.

**Zielrechner festlegen.** Das Skript hat keinen festen Zielrechner. Es liest
`PI_HOST` (SSH-Ziel `benutzer@adresse`, Pflicht – ohne bricht das Skript mit einer Meldung ab)
aus der Umgebung oder aus der lokalen, git-ignorierten Datei `deploy/.env.deploy`:

```bash
cp deploy/.env.deploy.example deploy/.env.deploy   # dann PI_HOST=… eintragen
# oder einmalig:
PI_HOST=admin@192.168.1.50 scripts/deploy_pi.sh
```

Weitere Werte (`DEPLOY_ROOT`, `CONTAINER`, `HEALTH_TIMEOUT_S`) sind dort ebenfalls
setzbar; gesetzte Umgebungsvariablen haben Vorrang vor der Datei.

**Eigener DNS-Resolver für den Container.** Ohne Angabe fragt der Container den Resolver
des Hosts, eine Vorgabe gibt es nicht (früher: Cloudflare 1.1.1.1/1.0.0.1). Wer einen
öffentlichen Resolver will, trägt ihn genauso ein. Braucht er einen lokalen Resolver (z. B. einen
Pi-hole, der interne Namen wie `cloud.home.example` auflöst), setzt man
`NODVARD_DECK_DNS_1` (und optional `NODVARD_DECK_DNS_2`) in `deploy/.env.deploy`. Die
alten Namen `LATTICE_DNS_1`/`LATTICE_DNS_2` gelten nur noch übergangsweise, spätestens bis 1.0 (sind beide gesetzt,
gewinnt der neue). Das Skript legt die Werte beim Ausliefern unter dem neuen Namen als `deploy/.env` auf dem
Zielrechner ab, wo `docker compose` sie liest. Stehen in einer bestehenden `deploy/.env` noch Zeilen
mit `LATTICE_`, benennt `pi_switch.sh switch` sie vor dem Umschalten auf `NODVARD_DECK_` um (steht
der neue Name schon da, fällt die alte Zeile weg) und behält die Datei vorher als
`deploy/.env.vor-umbenennung`. Ohne Skript (Rückfallweg) legt man `deploy/.env` dort selbst an.

**Rückfallweg, falls Docker Desktop nicht läuft** (baut auf dem Pi, dauert ~10 Min und
belastet ihn stark):

```bash
# ~/lattice-deploy-test ist der Standardordner (DEPLOY_ROOT); bei anderem DEPLOY_ROOT anpassen.
# Vor dem Auspacken den alten Ordner extensions/ löschen: sonst bleibt z. B. extensions/nexus-soc neben
# extensions/shield liegen, und Nodvard Deck startet wegen doppelter Migrationen nicht.
git archive -o /tmp/nodvard-deck.tar HEAD && scp /tmp/nodvard-deck.tar "$PI_HOST":~/nodvard-deck.tar
# Erst bauen (mit frisch geholten Basis-Images), dann umschalten: Das alte Dashboard läuft während des Baus weiter.
# `docker tag … previous` sichert den Rückweg; auf einem Rechner ohne bisherigen Stand diesen Schritt weglassen.
ssh "$PI_HOST" 'rm -rf ~/lattice-deploy-test/extensions && tar -xf ~/nodvard-deck.tar -C ~/lattice-deploy-test \
  && cd ~/lattice-deploy-test/deploy && docker tag nodvard-deck:latest nodvard-deck:previous \
  && docker compose -p deploy build --pull && docker compose -p deploy up -d --no-build'
# danach bis zu 4 Min: curl -s localhost:8080/api/v1/health  ->  "status":"ok"
# und: curl -s localhost:8080/api/v1/auth/bootstrap  ->  {"needed":false}  (sonst: Daten fehlen!)
# Klappt es nicht: bash pi_switch.sh rollback   (nodvard-deck:previous wieder in Betrieb)
# aufräumen: docker builder prune -f --reserved-space 2gb; docker image prune -f
```

Nach dem Bauen auf dem Pi trägt das Image den Namen `nodvard-deck:latest` (aus der Compose-Datei). Nicht mit
`--remove-orphans` aufräumen lassen: das wirkt auf das ganze Projekt.

Kaputter Stand nach einem Deploy: auf dem Pi im Deploy-Ordner
`bash pi_switch.sh rollback` (oder von Hand
`docker tag nodvard-deck:previous nodvard-deck:latest && docker compose -p deploy up -d --no-build`).
Zurück auf einen **älteren Commit** nicht mit dem alten `deploy_pi.sh` ausliefern (es kennt den
neuen Dienstnamen nicht und würde den alten Container neben dem neuen starten), sondern über
`nodvard-deck:previous` zurückgehen.

## Umgebungsvariablen: alte Namen umbenennen

Alle Einstellungen des Dashboards heißen jetzt `NODVARD_DECK_<NAME>` (früher
`LATTICE_<NAME>`), zum Beispiel `NODVARD_DECK_ENV`, `NODVARD_DECK_LOG_JSON`,
`NODVARD_DECK_DATA_DIR` und `NODVARD_DECK_DNS_1`/`_2` (nur Compose). `NODVARD_DECK_API_DOCS=1` schaltet die
API-Doku (`/docs`, `/redoc`, `/openapi.json`) auch außerhalb des Entwicklungsmodus ohne Anmeldung frei;
standardmäßig ist sie aus, angemeldete Admins bekommen das Dokument über `GET /api/v1/system/openapi.json`.
`NODVARD_DECK_MAX_BODY_BYTES` begrenzt die Größe einer Anfrage (Standard 1 MiB, darüber `413`; Uploads wie Sicherung,
Logo, Dokumente und Bilder haben eigene Grenzen), `NODVARD_DECK_FILES_MAX_UPLOAD_BYTES` die Größe eines Uploads im
Dateimanager (Standard 10 GiB, `0` = keine Grenze; meldet das Ziel zu wenig freien Platz, lehnt der Dateimanager den
Upload vorab mit `507` ab).

- **Die alten Namen gelten noch, aber nicht mehr lange: Eine der nächsten Versionen, spätestens 1.0, liest sie nicht
  mehr.** Eine vorhandene
  `.env`, eine eigene Compose-Datei oder ein `docker run -e LATTICE_…` laufen unverändert. Beim
  Start steht einmalig eine Warnung im Log, welche alten Namen noch benutzt werden, und der
  Inhaber sieht auf der Startseite einen Hinweis mit altem und neuem Namen (nur die Namen, nie die
  Werte). Umbenennen heißt: in der Compose-Datei oder `.env` `LATTICE_` durch `NODVARD_DECK_`
  ersetzen und den Container neu erstellen (`docker compose up -d`).
- **Danach gelten nur noch die neuen Namen.** Steht eine wichtige Einstellung wie Datenordner, Datenbank oder
  Schlüssel dann nur unter dem alten Namen, startet Nodvard Deck ohne sie, im schlimmsten Fall leer. Deshalb vorher
  umbenennen.
- **Nicht jeden alten Namen kann Deck selbst sehen.** `LATTICE_PORT` und `LATTICE_DNS_1`/`_2` liest nur Compose beim
  Einsetzen in die Compose-Datei, der Container bekommt sie nie. Sie fallen genauso weg, ohne Hinweis auf der
  Startseite: Steht einer davon in deiner `.env` oder deiner Umgebung, benenne ihn ebenfalls um, sonst läuft Deck
  ab 1.0 wieder auf Port 8080 bzw. mit dem Resolver des Rechners.
- **Der neue Name gewinnt.** Reihenfolge, vorne gewinnt: Umgebung (neu), Umgebung (alt),
  `.env` (neu), `.env` (alt). Die Umgebung schlägt also immer die `.env`.
- **Achtung bei eigenen Überschreibungen des Images:** Das Image legt jetzt
  `NODVARD_DECK_ENV`, `NODVARD_DECK_LOG_JSON` und `NODVARD_DECK_DATA_DIR` fest. Wer das mit
  `docker run -e LATTICE_DATA_DIR=…` (alter Name) überschreiben will, wird vom neuen
  Namen im Image überstimmt – dann bitte auf den neuen Namen umstellen.
- **Zurück aufs alte Image** (`nodvard-deck:previous`) geht ohne Änderung: Das alte Image bringt
  seine eigenen alten Werte mit und ignoriert die neuen Namen.

**Proxy für ausgehende Verbindungen** (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` im Container): Den nutzt nur die
Update-Suche bei ghcr.io. Erweiterungen (Proxmox, Nextcloud, ntfy usw.) und die Konsole einer VM oder eines Containers
verbinden sich immer direkt mit ihrem Ziel, damit Zugangsdaten und Konsolen-Ticket bei keiner anderen Stelle landen.
Kommt der Rechner nur über einen Proxy ins Internet, erreichen Erweiterungen externe Ziele wie `ntfy.sh` deshalb nicht;
Ziele, die der Rechner direkt erreicht (z. B. im eigenen Netz), gehen weiter.

## Backup / Restore

**Sicherungen in der Oberfläche** (Einstellungen → System → Sicherung): verschlüsselt (age),
ohne Anhalten des Dienstes, automatisch nach Zeitplan, mit Rotation und Download. Anleitung in
[docs/11-ERST-EINRICHTUNG.md §6.1](../docs/11-ERST-EINRICHTUNG.md#61-in-der-oberfläche-empfohlen).
Für einen Ordner außerhalb des Datenvolumes die vorbereitete Zeile `- ./sicherungen:/backups` in
der Compose-Datei (`compose.yml` bzw. `deploy/docker-compose.yml`) einkommentieren, für ein NAS oder NFS
siehe [Sicherungen auf ein NAS oder NFS](../docs/16-SICHERUNG.md#sicherungen-auf-ein-nas-oder-nfs). Die Skripte
unten bleiben und funktionieren wie bisher.

**Wiederherstellen** geht ebenfalls in der Oberfläche (Einstellungen → System → Wiederherstellen) und im
Einrichtungsassistenten einer frischen Installation („Sicherung einspielen“, mit dem Einrichtungscode).
Die Datei wird hochgeladen, geprüft und erst nach einer Bestätigung **beim nächsten Start** eingespielt:
`entrypoint.sh` ruft dafür `python -m nodvard_deck.boot` auf (es spielt eine vorgemerkte Sicherung ein und
migriert danach wie bisher). Der Dienst beendet sich zum Neustart mit dem Rückgabewert 75 – dafür braucht der
Container eine Neustart-Regel (`restart: unless-stopped`, in den mitgelieferten Compose-Dateien gesetzt; ein
`docker run` ohne `--restart` bleibt danach aus). Der alte Stand liegt danach unter `/app/data/restore/replaced-…`.
**`restore.sh` kann eine `.ndbak`-Datei vormerken** (`./restore.sh sicherung.ndbak`): es prüft sie mit
`python -m nodvard_deck.admin restore-backup`, fragt das Passwort ab und startet den Dienst auf Wunsch neu; eingespielt
wird dann beim Start. Der Container bekommt dafür eine Kopie in einem eigenen Ordner unter `/tmp` (bzw. `$TMPDIR`), die
andere lokale Benutzer nicht lesen können; das Skript löscht sie am Ende wieder. So klappt es auch mit einer Datei, die
root gehört (dann mit `sudo`), oder in einem Ordner, den der Container-Benutzer 1000 nicht betreten darf. Wer das Skript
weder als root noch als UID 1000 startet, braucht `setfacl`, sonst bricht es ab und verlangt `sudo`. Liegt noch die
Marke eines unterbrochenen `.tar.gz`-Einspielens (siehe unten), merkt es nichts vor (Exit-Code 3). **Nach einem Rückweg (`pi_switch.sh rollback`) auf ein Image von vor der Paket-Umbenennung**
kennt das Image nur den alten Modulnamen; dann statt `restore.sh` direkt
`docker compose -p deploy run --rm --no-deps --user 1000:1000 --entrypoint python -v "$PWD:/backup:ro" nodvard-deck -m lattice.admin restore-backup /backup/<datei>.ndbak`
verwenden (`.tar.gz`-Sicherungen betrifft das nicht, die spielt `restore.sh` ohne Python ein). Das ist der Weg ohne Oberfläche. Hochladen größer als 4 GiB (`NODVARD_DECK_RESTORE_MAX_UPLOAD_BYTES`)
oder mehr als 200 000 Dateien (`…_MAX_ENTRIES`) wird abgelehnt; entpackt dürfen es höchstens 16 GiB
(`…_MAX_UNPACKED_BYTES`) und nie mehr als der freie Platz sein. Zusätzlich verlangt das Verschieben, dass der
Datenordner (Datenbank, Schlüssel, `ext`, `branding`, `runs`) auf **einem** Laufwerk liegt (`rename`).

Der Dienst heißt in Compose `nodvard-deck` (vor 0.6 `lattice`). Für einen Stand von vor 0.6 (Container
`deploy-lattice-1`) gibt es kein passendes `backup.sh` mehr; dort wird von Hand gesichert, bei angehaltenem Dienst:
`docker stop deploy-lattice-1 && docker run --rm --user 1000:1000 -v deploy_lattice_data:/data -v "$PWD":/backup --entrypoint tar lattice:latest czf /backup/vorher.tar.gz -C /data . ; docker start deploy-lattice-1`

```bash
cd deploy
./backup.sh [zielverzeichnis]              # nodvard-deck-backup-<timestamp>.ndbak (mit Sicherungsschlüssel) oder .tar.gz
./restore.sh /pfad/zu/nodvard-deck-backup-*.ndbak     # fragt Sicherungspasswort oder Wiederherstellungsschlüssel ab
./restore.sh /pfad/zu/nodvard-deck-backup-*.tar.gz    # auch ältere lattice-backup-*.tar.gz
```

**Verschlüsselung:** Ist in Nodvard Deck ein Sicherungsschlüssel eingerichtet (Einstellungen → System → Sicherung),
schreibt `backup.sh` eine verschlüsselte Sicherung wie aus der Oberfläche (`nodvard-deck-backup-<timestamp>.ndbak`,
erzeugt von `python -m nodvard_deck.admin create-backup` im Hilfscontainer). `restore.sh` merkt sie wie jede `.ndbak`
zum Einspielen vor und fragt dabei das Sicherungspasswort oder den Wiederherstellungsschlüssel ab. **Ohne Schlüssel**
(oder mit einem Image, das den Befehl noch nicht kennt) entsteht wie bisher ein `.tar.gz`, und das Skript warnt vor dem
Anhalten und am Ende deutlich: **Das `.tar.gz` ist nicht verschlüsselt und enthält `master.key`.** Wer die Datei hat,
kann alle gespeicherten Zugangsdaten entschlüsseln. Bewahre es auf wie ein Passwort (nicht in Cloud-Ordner, Chats oder
unverschlüsselte Mails), oder richte den Sicherungsschlüssel ein. Lässt sich gar nicht feststellen, ob es einen Schlüssel
gibt (Datenbank unlesbar), bricht `backup.sh` ab, bevor es etwas anhält. Bei Erfolg steht in der Ausgabe die Zeile
`Fertig: <Pfad der Sicherung>`.

Liegt noch ein Stand von vor 0.6 auf dem Rechner (`deploy-lattice-1` läuft, oder `nodvard-deck:latest` fehlt), brechen
beide Skripte ab, bevor sie etwas anlegen oder anhalten (siehe [Stand von vor 0.6](#stand-von-vor-06)).

Beide stoppen den Dienst kurz für eine garantiert konsistente Kopie des gesamten
`/app/data`-Baums (SQLite inkl. WAL/SHM, `master.key`, `jwt_secret.key`,
`vault_keyring.json`, Extension-Daten, Script-Git-Repo) — für die in
D-02 ([docs/00-DECISIONS.md](../docs/00-DECISIONS.md))
beschriebene Last (eine Handvoll Nutzer, ein paar Dutzend Hosts) ist die kurze
Ausfallzeit (Sekunden) der einfachste Weg, keine inkonsistente Kopie zu ziehen.

**Rechte:** Ohne Zugriff auf Docker (nicht root, nicht in der Gruppe `docker`) beide mit `sudo` starten. Sonst brechen
sie ab, bevor sie etwas anhalten oder verändern: `backup.sh` mit „Docker antwortet nicht …“, `restore.sh` mit „Der
Datenordner liess sich nicht pruefen“. Mit `sudo` gestartet gehören die Sicherung und ein dabei neu angelegter
Zielordner dem Aufrufer (`SUDO_UID`), als root ohne sudo (etwa aus der root-Crontab) dem Besitzer des Zielordners,
wenn das nicht root ist. Eine `.tar.gz` liest `restore.sh` selbst und reicht sie über die Standardeingabe in den
Container; eine Datei, die root gehört, geht also mit `sudo`.

**`backup.sh`** prüft vor dem Anhalten, ob das Image `nodvard-deck:latest` da ist, ob sich in den Zielordner
schreiben lässt und ob es einen Sicherungsschlüssel gibt. Die Datei entsteht unter einem Zwischennamen
(`<name>.partial.<Zufall>`, Rechte 0600) und bekommt ihren Namen erst, wenn sie mindestens 1 KiB groß und ein lesbares
tar.gz bzw. eine Sicherung von Nodvard Deck (Kopfzeile) ist; sonst wird sie gelöscht.

**Sperre:** Beide Skripte (auch `restore.sh` mit `.ndbak`) halten bis zum Ende dieselbe Sperre (`flock` auf
`deploy/.backup-restore.lock`). Läuft schon eine Sicherung oder ein Einspielen (etwa eine Cron-Sicherung), bricht der
zweite Lauf mit Exit-Code 4 ab, ohne etwas zu verändern. Fehlt `flock` (Paket util-linux), läuft es mit einer Warnung
ohne Sperre.

**Einspielen einer `.tar.gz`** (ersetzt den kompletten Inhalt von `/app/data`, fragt vorher nach): `restore.sh` prüft
die Datei mit `tar tzf`, hält den Dienst an und entpackt sie nach `/app/data/.restore-neu`; die bisherigen Daten
bleiben dabei unberührt. Scheitert das (Platz, Lesefehler, Strg+C), startet der Dienst wieder auf dem alten Stand
(außer beim Fertigmachen eines abgebrochenen Austauschs, siehe unten: dann bleibt er gestoppt). Erst danach tauscht es
per Umbenennen innerhalb des Volumes aus (alte Daten nach `.restore-alt`, neue an ihre Stelle, `.restore-alt` weg);
kurz liegen die Daten doppelt im Volume. Die Marke `/app/data/.restore-austausch` hält fest, dass und wie weit der
Austausch läuft. Bricht er mittendrin ab, bleibt der Dienst gestoppt; derselbe Aufruf mit derselben Datei macht ihn an
der richtigen Stelle fertig und startet den Dienst. Solange die Marke liegt, lehnt `backup.sh` ab (Exit-Code 3); ist
sie leer oder beschädigt, brechen beide Skripte mit Exit-Code 5 ab, und die Meldung nennt den Weg von Hand. Liegt beim
Beginn eines neuen Austauschs noch ein `.restore-alt` von früher, wird es nicht gelöscht, sondern als
`.restore-alt-<Zeit>` beiseitegelegt. Arbeitsordner und Marke kommen in keine Sicherung. SIGHUP (SSH-Abbruch) hält
das Skript nicht an, den Austausch auch Strg+C nicht. Die Meldungen ab der Rückfrage stehen zusätzlich in
`deploy/restore.log` (anderer Ordner: `RESTORE_LOG_DIR`); das Protokoll und eine dabei neu angelegte Sperrdatei
gehören nach `sudo` dem Aufrufer.

**Backup ohne Stopp (fortgeschritten, nicht in `backup.sh` automatisiert):** SQLites
eigene Online-Backup-API erzeugt eine konsistente Momentaufnahme auch bei laufendem
Betrieb:

```bash
# Das Image hat keine sqlite3-Kommandozeile (python:3.12-slim), Pythons sqlite3-Modul reicht:
docker compose exec -u lattice nodvard-deck python -c "import sqlite3; s=sqlite3.connect('/app/data/lattice.db'); d=sqlite3.connect('/app/data/_snapshot.db'); s.backup(d); d.close(); s.close()"
docker compose cp nodvard-deck:/app/data/_snapshot.db ./lattice-db-snapshot.db
chmod 600 ./lattice-db-snapshot.db   # die Kopie enthält die ganze Datenbank, nur für dich lesbar
docker compose exec -u lattice nodvard-deck rm /app/data/_snapshot.db
```

Beide Skripte lassen den Hilfscontainer bewusst **nicht** durch `/entrypoint.sh` laufen
(`--entrypoint tar` bzw. `--entrypoint sh`): sonst würde vor dem Sichern oder Einspielen
die Datenbank-Migration laufen. Ein „Vorher-Backup“ nach einem neuen Build wäre dann
schon migriert, und `restore.sh` würde an genau der kaputten Datenbank scheitern, die es
ersetzen soll. Ein `trap` startet den Dienst auch bei einem Fehler wieder, außer solange ein Austausch der Daten durch
`restore.sh` nicht fertig ist (siehe oben).

Das sichert nur `lattice.db` selbst, nicht `master.key`/`jwt_secret.key`/
Extension-Daten/Script-Repo — für einen vollständigen Restore-Punkt weiterhin
`backup.sh` verwenden.

## Aktualisieren: Kopie vor der Migration, Notseite, Rückweg

**Gibt es eine neue Version?** Steht unter Einstellungen → System → „Updates“. Das Dashboard fragt einmal am Tag bei ghcr.io nach
(abschaltbar; ghcr.io und die Freigabe am Release auf github.com gehören zu GitHub, das dabei die IP-Adresse und den Zeitpunkt sieht, sonst nichts, auch nicht die installierte Version), „Jetzt suchen“ geht
jederzeit. Dort stehen auch die Anleitungen für Docker Compose, Docker Desktop, Portainer, Synology, Unraid und `scripts/deploy_pi.sh`.
Mit dem [Update-Helfer](#update-helfer) geht das Einspielen dort per Knopf; ohne ihn wie folgt von Hand.
Mit dem fertigen Image (Datei `deploy/compose.standalone.yml`, bei der Installation als `compose.yml` abgelegt) reicht im Ordner der Datei:

```bash
docker compose pull
docker compose up -d
```

Heißt die Datei anders (z. B. noch `compose.standalone.yml`), `-f <Dateiname>` anhängen. Vorher die laufende Version notieren (steht
in der Karte), für den Rückweg unten. Vorabversionen (`0.7.0-rc1`, Kanal „Beta“) gibt es nicht unter `:latest`, nur unter ihrem genauen Tag.

Das Release-Image trägt die Datei `/app/image-info.json` mit Herkunft und genauer Version (`{"image": "ghcr.io/nodvard/deck",
"version": "<Version>"}`, aus den Build-Argumenten `IMAGE` und `VERSION`, geschrieben von `python -m nodvard_deck.image_info`). Das Image
von Nodvard Dock trägt zusätzlich `"edition": "dock"` (Build-Argument `EDITION`): Name, Dienstname und Update-Suche richten sich danach. Ein selbst
gebautes Image (z. B. über `deploy_pi.sh`) hat sie nicht; die Oberfläche zeigt dann den Weg über `deploy_pi.sh`. Bewusst eine Datei und
keine Umgebungsvariable im Image: Die käme beim Neuanlegen eines Containers mit dessen alten Einstellungen (Portainer „Recreate“,
Update-Helfer) in den neuen Container mit und zeigte dort die alte Version. Eine gesetzte Variable `NODVARD_DECK_IMAGE` bzw.
`NODVARD_DECK_BUILD` geht der Datei vor (leer zählt als nicht gesetzt) – normalerweise braucht es sie nicht.

Beim Start läuft vor der Anwendung `python -m nodvard_deck.boot` (`entrypoint.sh`). Es macht, in dieser Reihenfolge:
vorgemerkte Wiederherstellung einspielen, **Kopie der Datenbank anlegen, wenn eine Migration ansteht**, migrieren.

- **Kopie vor jeder Migration** (SQLite): `/app/data/backups/vor-update/<Zeit>_<von>_<nach>.db` (+ `.json`), die **letzten drei**
  bleiben. Vorher muss mindestens das **1,2-fache der Datenbank** frei sein, sonst gibt es **keine Migration** (und die Notseite sagt, wie viel fehlt).
  **Ohne Kopie wird nie migriert.** Notausgang (nicht empfohlen): `NODVARD_DECK_SKIP_PRE_MIGRATE_BACKUP=1`
  (alter Name `LATTICE_SKIP_PRE_MIGRATE_BACKUP`). Nicht Teil der Kopie: Dateien außerhalb der Datenbank (Erweiterungsdaten, Schlüssel); Migrationen fassen
  sie nicht an. Bei einer anderen Datenbank als SQLite gibt es keine Kopie (bitte vorher selbst sichern).
  Die Oberfläche zeigt die letzten Kopien unter Einstellungen → System → „Kopien vor Updates“ (`GET /api/v1/system/info`).
- **Scheitert die Migration**, geht die Datenbank auf die Kopie zurück (die Daten sind danach wie vor dem Update), und statt der Anwendung läuft die **Notseite**.
  Der halbe Stand wird dabei verworfen (er enthält nichts, was nicht auch in der Kopie steht).
  **Bricht sie mittendrin ab** (Strom weg, `docker restart`), macht der nächste Start dasselbe und migriert dann neu; der halbe Stand bleibt dann 30 Tage unter
  `restore/replaced-…`. Bei der allerersten Migration einer neuen Installation (noch ohne Konto) wird der halbe Stand dort abgelegt und frisch begonnen.
- **Neuere Daten werden beim Zurücksetzen nie gelöscht:** Was der automatische Rückweg auf die alte Version oder „Stand vor dem Update wiederherstellen“ ersetzt, liegt
  30 Tage unter `restore/replaced-…`. Liegt die Datenbank auf einem eigenen Laufwerk (eigener `NODVARD_DECK_DATABASE_URL`), wird sie dafür in den Datenordner kopiert;
  fehlt dort der Platz, zeigt die Notseite „zu wenig Platz“, und es wurde nichts verändert. Nur den halben Stand einer abgebrochenen Migration verwirft der Start in
  diesem Fall, statt auf der Notseite hängen zu bleiben.
- **Wiederherstellung + abgebrochene Migration:** Bricht die Migration einer gerade eingespielten Sicherung ab, nimmt der nächste Start das Einspielen zurück; es gilt
  wieder genau der alte Stand (Datenbank, Schlüssel, Migrationsstand in `.boot/state.json`, `pre_restore`).
- **Notseite** (`python -m nodvard_deck.rescue`, nur Standardbibliothek, auf dem Port der Anwendung): `GET /api/v1/health` antwortet **503** `{"status":"rescue"}`
  (nie `ok`: der Docker-Healthcheck schlägt an, `scripts/deploy_pi.sh` schaltet zurück). Sie lauscht auf `--host`/`--port` aus dem Startbefehl (sonst `UVICORN_HOST`);
  ohne erkennbare Adresse nur auf `127.0.0.1`, wie uvicorn selbst (das Image setzt `--host 0.0.0.0`). Jede Verbindung endet nach 10 s, die Kopfzeilen müssen nach 3 s da sein (Anfragezeile und Kopfzeilen zusammen höchstens
  32 KiB, dabei unter 100 Kopfzeilen, sonst `431`). Je Rechner höchstens 8 Verbindungen gleichzeitig (bei IPv6 je /64-Netz), insgesamt 64; `127.0.0.1` und `::1`
  (Healthcheck im Container) haben 8 eigene Plätze.
  Ohne Code zeigt sie nur Allgemeines. Mit dem **Notfallcode**
  (steht als Banner im Protokoll des Containers: `sudo docker compose logs nodvard-deck | grep Notfallcode`, und in `.boot/rescue_code.txt` im Datenordner) zeigt sie Grund und
  bereinigtes Protokoll und bietet „Neu versuchen“ (Container endet mit 75, die Neustart-Regel startet ihn neu) und, wo es geht, „Stand vor dem Update
  wiederherstellen“. Die Eingabe des Codes ist gedrosselt (5 Fehlversuche je Rechner, insgesamt 25 in 10 Minuten);
  die Sperre weist nur falsche Codes ab, der richtige Code geht immer sofort durch. Fällt die Notseite selbst aus, **wartet der Container** (`sleep`) – er läuft nie in eine Neustart-Schleife;
  `docker stop` dauert dann die üblichen 10 Sekunden.
- **Zurück auf die alte Version** (nach einem Update, das nicht klappt): Image-Version in der Compose-Datei auf die vorherige zurückstellen und den Container neu starten
  (`deploy_pi.sh` und `pi_switch.sh rollback` tun das für das Image selbst).
  * Ist die neue Version **nie erfolgreich gestartet**, spielt die alte die Kopie **von selbst** wieder ein (die neueren Daten bleiben 30 Tage unter `restore/replaced-…`).
    Ob sie gestartet ist, hält die Anwendung beim Start fest (`started_ok`); klappt das nicht sofort (Speicher voll), versucht sie es jede Minute erneut.
  * Hat die neue Version schon gearbeitet, zeigt die alte die Notseite („Die Daten sind neuer als diese Version“). Dort: **Stand vor dem Update wiederherstellen**
    (Änderungen seit dem Update gehen verloren; die neueren Daten bleiben 30 Tage unter `restore/replaced-…`) oder wieder die neue Version eintragen.
  * **Der Rückweg funktioniert erst auf Versionen, die diese Funktion schon enthalten.** Eine ältere Version (bis 0.5.x) kann eine Kopie nicht selbst einspielen und kennt die Notseite nicht;
    die Vorversion muss also mindestens die Version mit „Kopie vor jeder Migration“ sein. Beim ersten Update **auf** eine solche Version legt diese die Kopie schon an; zurück geht es danach.
    Scheitert die Migration, steht die Datenbank allerdings sofort wieder auf dem alten Stand – dann läuft auch eine ältere Version weiter.
- **Zurück von 0.7 auf 0.6.x (Nodvard Shield heißt technisch seit 0.7 `shield` statt `nexus-soc`):** 0.7 bringt keine Migration, der Rückweg
  braucht also keine Kopie und keine Notseite. Was danach passiert, hängt davon ab, wie die Installation entstanden ist:
  * **Bestehende Installation** (schon vor 0.7 eingerichtet): Shield nutzt weiter die gespeicherte Zeile `nexus-soc`. Nach dem Rückweg ist alles
    da, Shield läuft. Nur Vorschläge, die unter 0.7 entstanden sind, zeigt das alte Image nicht auf den Shield-Seiten; unter „Aktionen“ stehen sie
    und lassen sich weiter freigeben. Ein erneutes Update auf 0.7 geht ohne Verlust.
  * **Neu mit 0.7 eingerichtet** (Zeile `shield`): Das alte Image kennt diese Kennung nicht. Unter 0.6.1 steht Shield als „Fehler“ (fehlt) da, dazu
    legt 0.6.1 eine ausgeschaltete Zeile `nexus-soc` ohne Einstellungen an; Shield ist im alten Image also aus. Nach dem erneuten Update auf 0.7 ist
    alles wieder da. 0.6.0 setzt die Zeile `shield` auf „Fehler“; nach dem erneuten Update auf 0.7 Shield dann einmal von Hand einschalten
    (Einstellungen → Erweiterungen). Was in der Zeit mit dem alten Image unter `nexus-soc` eingestellt wurde, kommt nicht mit.
  * **Sicherungen:** Eine Sicherung aus 0.5 oder 0.6.x lässt sich in 0.7 einspielen. Eine Sicherung aus einer mit 0.7 neu eingerichteten Installation
    lässt sich in 0.6.x einspielen, das alte Image warnt aber „Diese Erweiterungen aus der Sicherung gibt es hier nicht: shield …“ und zeigt die Einstellungen von
    Shield nicht; nach dem
    Update auf 0.7 sind sie wieder da.
- **Zurück von 0.9 auf 0.8 (das Modul Docker heißt technisch seit 0.9 `docker` statt `service-matrix`):** Dafür bringt 0.9 keine
  Migration, der Rückweg braucht also keine Kopie und keine Notseite.
  * **Bestehende Installation** (schon vor 0.9 eingerichtet): Docker nutzt weiter die gespeicherte Zeile `service-matrix`. Nach dem
    Rückweg ist alles da: Einstellungen, eigene Kachel-Adressen, der Zeitplan der Image-Prüfung und laufende Image-Updates. Nur
    Meldungen, die unter 0.9 entstanden sind, verlinken auf `/ext/docker/matrix`; diese Seite kennt das alte Image nicht. Ein
    erneutes Update auf 0.9 geht ohne Verlust.
  * **Neu mit 0.9 eingerichtet** (Zeile `docker`): Das alte Image kennt diese Kennung nicht und zeigt Docker unter Einstellungen →
    Erweiterungen als „Fehler“, dazu legt es eine ausgeschaltete Zeile `service-matrix` ohne Einstellungen an; Docker ist im alten
    Image also aus. Nach dem erneuten Update auf 0.9 läuft es wieder mit seinen Einstellungen, ohne Einschalten von Hand. Was in
    der Zeit mit dem alten Image unter `service-matrix` eingestellt wurde, kommt nicht mit.
  * **Sicherungen:** Eine Sicherung aus 0.8 lässt sich in 0.9 einspielen. Eine Sicherung aus einer mit 0.9 neu eingerichteten
    Installation lässt sich in 0.8 einspielen, das alte Image warnt aber „Diese Erweiterungen aus der Sicherung gibt es hier nicht:
    docker …“; nach dem Update auf 0.9 sind die Einstellungen wieder da.
- **Sperre:** Die laufende Anwendung hält `/app/data/.boot/app.lock`. Ein zweiter Container mit demselben Datenordner (z. B. `docker compose run nodvard-deck …` neben dem laufenden Dienst)
  ändert deshalb nichts: `boot` beendet sich mit Code 75. Für Wartungsbefehle lieber `docker compose exec`, oder `run --entrypoint …` bei gestoppter Anwendung (so machen es `backup.sh` und `restore.sh`).
- **Healthcheck:** `--start-period` ist **300 s** (Image und beide Compose-Dateien), damit eine lange Migration samt Kopie nicht als „unhealthy“ gilt.
  Ohne `user:` läuft der Check als root; deshalb ruft er `python -I` auf (isoliert: ohne den aktuellen Ordner im Suchpfad, ohne `PYTHON*`-Variablen und
  ohne Pakete aus dem Benutzerverzeichnis). Eine eigene Compose-Datei mit eigenem `healthcheck:` sollte das `-I` übernehmen.
- **Alter Stand einer Wiederherstellung oder eines Rückwegs** (`restore/replaced-…`, kann Konten und Schlüssel im Klartext enthalten): wird **nach 30 Tagen automatisch gelöscht**; die Oberfläche nennt den Tag und kann ihn früher löschen.

## Update-Helfer

Mit dem Update-Helfer spielst du Updates unter Einstellungen → System → „Updates“ per Knopf ein und kannst danach 7 Tage
lang einmal per Knopf zurück (Karte „Kopien vor Updates“). Der Helfer ist ein kleiner eigener Dienst `updater` in
derselben Compose-Datei wie Nodvard Deck. Er läuft nur, wenn du ihn **bewusst einschaltest**: mit
[`compose.standalone-mit-helfer.yml`](compose.standalone-mit-helfer.yml) statt `compose.standalone.yml`. Bedienung in der
Oberfläche: [docs/11-ERST-EINRICHTUNG.md, Anhang I.2](../docs/11-ERST-EINRICHTUNG.md#i2-update-per-knopf-update-helfer).

> **Achtung:** Der Helfer bekommt Zugriff auf Docker (den Docker-Socket). Das ist so viel wie root auf diesem Rechner.
> Darum ist er streng gebaut: kein Netz, kein offener Port, nur lesbar, ohne Sonderrechte, mit Speicher- und
> Prozessgrenze. Vom Dashboard nimmt er nur Aufträge an (Aktion und Version), prüft alles selbst und tauscht nur den
> Dienst `nodvard-deck` im selben Compose-Projekt gegen eine neuere **offizielle** Version (`ghcr.io/nodvard/deck`).
> Willst du das nicht, bleib bei `compose.standalone.yml` und aktualisiere von Hand (siehe oben).

**Image:** `ghcr.io/nodvard/deck-updater`. Es entsteht bei jedem Versions-Tag im Job `updater-image` von
`.github/workflows/release.yml` (für `linux/amd64` und `linux/arm64`, Tag `:<version>`). Das Tag `1` bekommt eine Version
erst, wenn ihr Selbsttest auf beiden Plattformen bestanden ist, und nie eine Vorabversion; es gilt für alle Helfer mit
derselben Schnittstelle. Gebaut wird aus `deploy/updater/Dockerfile`, nur aus der Standardbibliothek von Python.

**Welche Datei:** Am [GitHub-Release](https://github.com/nodvard/deck/releases/latest) einer fertigen Version hängt
`compose.standalone-mit-helfer.yml`. Sie ist dieselbe Datei wie im Repository, nennt den Helfer aber genau per Digest
(`ghcr.io/nodvard/deck-updater@sha256:…`), also genau den Bau, dessen Selbsttest bestanden ist und auf den in dem Moment
`:1` zeigt. Den Digest nennt auch der Text des Releases. Nimm für neue Installationen und für Updates des Helfers diese
Datei. Beim Versions-Tag kommt sie zunächst an einen Entwurf des Releases; abrufbar ist sie erst, wenn die Version auf
GitHub veröffentlicht ist. Bis dahin liefert der Link auf das neueste Release noch die Datei der Version davor. Im
Repository steht weiter `:1`, für Entwicklerstände und für alle, die immer den neuesten Helfer wollen. Ältere Releases
haben die Datei noch nicht: Meldet `curl` unten 404, nimm die aus dem Repository,
`https://raw.githubusercontent.com/nodvard/deck/main/deploy/compose.standalone-mit-helfer.yml`.

**Was er kann:**
- Update auf die neueste fertige Version, wenn Nodvard Deck mit dem offiziellen Image und beweglichem Tag läuft
  (`ghcr.io/nodvard/deck:latest` oder eine Reihe wie `:0.7`). Die Docker-Engine lädt die neue Version selbst, der Helfer
  hat kein Netz. Geladen wird genau das Image, das die Update-Suche gefunden hat (per Digest). Zeigt das Tag in der
  Registry inzwischen auf etwas anderes, lädt er nichts und bittet, erneut nach Updates zu suchen.
- Freigabe prüfen: Jede fertige Version bekommt am Release eine Freigabe (`freigabe.json`), die Nico mit seinem
  Schlüssel signiert. Das Dashboard bietet ein Update per Knopf nur mit Freigabe an und schickt sie mit; der Helfer
  prüft die Signatur mit den Schlüsseln, die fest in ihm stehen, und lehnt eine gefälschte oder fremde Freigabe ab.
  Ein älterer Helfer kennt die Freigabe noch nicht; die Karte „Updates“ sagt dann, dass du ihn erneuern solltest.
- Startet die neue Version nicht (beendet sich, startet immer wieder neu, zeigt die Notseite oder ist nach 15 Minuten
  nicht bereit), schaltet er von selbst auf die alte Version zurück.
- Rückweg per Knopf auf die Version davor: einmal, 7 Tage lang. Hat die neue Version die Datenbank umgebaut, gehen die
  Daten mit zurück (die Oberfläche sagt das vorher und verlangt eine Bestätigung).
- Nach einem Absturz oder Neustart des Rechners macht er einen angefangenen Vorgang zu Ende oder baut ihn zurück.
- Jedes Ergebnis steht genau einmal im Protokoll, wichtige kommen zusätzlich als Meldung.

**Was er (noch) nicht kann:**
- Nur Installationen mit Docker Compose (Kommandozeile, Docker Desktop, Portainer-Stack, Synology-Projekt, Unraid mit
  Compose-Plugin). Einzeln über eine Oberfläche angelegte Container gehen nicht.
- Kein Update bei fester Version des Dienstes `nodvard-deck` in der Compose-Datei (`:0.7.0`, `@sha256:…`), bei
  Vorabversionen und bei einem selbst gebauten Image (etwa über `scripts/deploy_pi.sh`); die Karte sagt dann, warum der
  Knopf fehlt.
- Ziele erst ab Version 0.7.0: Das erste Update per Knopf geht von 0.7.0 auf die Version danach.
- Sich selbst aktualisieren: Das machst du von Hand (siehe unten).
- Die Freigabe ist noch nicht Pflicht: Ein älteres Dashboard, das keine mitschickt, bekommt ein Update weiter ohne
  sie. Pflicht wird sie mit Version 1.0. Der Rückweg kommt ohne Freigabe aus, er setzt nur die Version davor wieder ein,
  die der Helfer selbst gesichert hat.

**Voraussetzungen**
- Nodvard Deck ab 0.7.0, installiert wie unter „Installation ohne das Repo“.
- Der Dienst heißt `nodvard-deck`. Umbenannt? Dann beim Helfer `NODVARD_DECK_UPDATER_SERVICE` auf den neuen Namen setzen
  (Zeile in der Compose-Datei einkommentieren).

**Einrichten – Kommandozeile (Linux, Docker Desktop)**
1. In den Ordner mit deiner `compose.yml` wechseln und sie sichern: `cp compose.yml compose.yml.alt`. Eigene Änderungen
   merken: Port, Sicherungsordner, DNS und vor allem einen Ordner statt des Datenspeichers (z. B. `./daten:/app/data`).
   Fehlt der in der neuen Datei, startet Nodvard Deck mit leeren Daten (deine Daten bleiben im Ordner liegen).
2. Datei mit Helfer laden (ersetzt die alte; Projektname und Datenspeicher bleiben gleich), eigene Änderungen übernehmen
   und starten:

   ```bash
   curl -fsSL -o compose.yml https://github.com/nodvard/deck/releases/latest/download/compose.standalone-mit-helfer.yml
   docker compose up -d
   ```

3. Unter Einstellungen → System → „Updates“ steht nach kurzer Zeit „Update-Helfer: bereit“. Bei „nicht bereit“ nennt die
   Karte den Grund und was hilft.

Rootless Docker: neben die `compose.yml` eine Datei `.env` mit `NODVARD_DECK_DOCKER_SOCKET=/run/user/1000/docker.sock`
legen (`echo $XDG_RUNTIME_DIR/docker.sock` zeigt deinen Pfad).

**Einrichten – Portainer und Synology:** den Inhalt deines Stacks bzw. Projekts durch `compose.standalone-mit-helfer.yml`
vom neuesten Release ersetzen, eigene Änderungen übernehmen (vor allem einen Ordner statt des Datenspeichers) und neu starten (Portainer:
„Update the stack“). Der Helfer muss im **selben** Stack bzw. Projekt stehen, nicht als eigener. Anderer Socket: in
Portainer unter „Environment variables“ `NODVARD_DECK_DOCKER_SOCKET` setzen.

**Selbsttest:** Ob das Image vollständig und lauffähig ist, prüft es ohne Docker-Zugang, Kanal und Zustand:

```bash
docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges \
  ghcr.io/nodvard/deck-updater:1 --selftest     # Ausgabe: eine Zeile mit "ok":true
```

Aus dem Quellcode geht dasselbe im Ordner `deploy/updater` mit `python -m nodvard_deck_updater --selftest` (im Image läuft
es als `python -I -m nodvard_deck_updater --selftest`).

**Helfer aktualisieren** (nicht, während er gerade ein Update macht): Die Zeile `image:` beim Dienst `updater` aus
`compose.standalone-mit-helfer.yml` vom [neuesten Release](https://github.com/nodvard/deck/releases/latest) in deine
Compose-Datei übernehmen, dann `docker compose up -d updater` (Synology: per SSH im Projektordner, mit `sudo` davor;
Portainer: Zeile im Stack ändern, „Update the stack“). Den Digest kannst du mit dem Text des Releases vergleichen. Steht
bei dir noch `:1` (Datei aus dem Repository), reicht `docker compose pull updater && docker compose up -d updater`. Das
Modul Docker bietet für das Helfer-Image kein „Einspielen“ an.

**Helfer ausschalten oder entfernen**
- Vorübergehend: `docker compose stop updater`. Die Karte zeigt dann „antwortet nicht“ und die Anleitung für Updates von
  Hand. Beim nächsten `docker compose up -d` läuft er wieder.
- Entfernen: wieder `compose.standalone.yml` als `compose.yml` nehmen (eigene Änderungen übernehmen), dann
  `docker compose up -d --remove-orphans`. Danach dürfen die Volumes des Helfers weg:
  `docker volume rm nodvard-deck-updater nodvard-deck-updater-state`. Der Rückweg per Knopf ist damit weg, deine Daten
  (`nodvard-deck-data`) bleiben.

**Startet Nodvard Deck nach einem Update gar nicht mehr** (und der Helfer hat nicht von selbst zurückgeschaltet): in der
Compose-Datei die Vorversion eintragen (z. B. `ghcr.io/nodvard/deck:0.7.0`), `docker compose up -d`, dann auf der Notseite
„Stand vor dem Update wiederherstellen“ (siehe oben). Läuft alles wieder: zurück auf `ghcr.io/nodvard/deck:latest` und
`docker compose up -d`. Solange die feste Version eingetragen ist, bleibt der Knopf aus.

**Hinweis für die Veröffentlichung:** Die Pakete `deck` und `deck-updater` der Organisation `nodvard` in der
GitHub-Container-Registry (`ghcr.io/nodvard/deck`, `ghcr.io/nodvard/deck-updater`) müssen auf GitHub einmal auf
**Public** gestellt werden (Paket → „Package settings“ → „Change visibility“). Neue Pakete sind dort oft erst privat: Dann
kann ein fremder Rechner die Images nicht ziehen, und die Update-Suche bekommt kein anonymes Token. Nach dem ersten
Release mit dem Helfer also bei `deck-updater` nachsehen.

## Demo-Modus

`NODVARD_DECK_DEMO_MODE=1` (alt: `LATTICE_DEMO_MODE=1`) legt beim Start dieselben Beispieldaten an wie der
Knopf „Mit Beispieldaten ansehen“ im Cockpit (D-10): fünf Beispiel-Server aus dem Dokumentationsbereich
`192.0.2.0/24` (ohne Zugang, es wird nie etwas im Netz angefragt) und vier Meldungen. Das passiert nur,
solange es noch keinen echten Server gibt, und nur einmal. In der Seitenleiste steht „Demo-Modus“, oben
ein Band mit „Beispieldaten löschen“. Wer das Image nur zum Ansehen braucht, setzt das Flag; alle anderen
brauchen es nicht – den Knopf gibt es auf jeder frischen Installation. Siehe
[docs/11-ERST-EINRICHTUNG.md, Anhang E.2](../docs/11-ERST-EINRICHTUNG.md#e2-beispieldaten).

## Hardware

Nodvard Deck läuft auf einem Raspberry Pi 4 oder 5 mit 4 GB RAM (64-Bit-System, `linux/arm64`)
und auf jedem x86-64-Rechner mit Docker. Im Leerlauf braucht der Container rund 100 MB
Arbeitsspeicher und kaum CPU; das Image ist knapp 400 MB groß. Auf einem Pi bleibt damit genug
Luft für weitere Dienste. Nur das Bauen des Images ist für einen Pi zu schwer, dafür siehe
„Deploy auf einen eigenen Pi per SSH“.