Ctrl K
ComunidadGuías…

deploy/entrypoint.sh

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

Archivo original ↗Preguntar a KiBot
#!/bin/sh
# Warum ein Entrypoint: Das Image kopiert Alembic (backend/alembic.ini, backend/migrations/)
# zwar mit, uvicorn allein führt die Migration aber nie aus -- ein frisches /app/data-Volume
# hat keine Tabellen, der allererste Zugriff (`ensure_builtin_roles()` in main.py's lifespan)
# scheitert mit "no such table: roles", der Prozess stirbt. Im Entwicklungsbetrieb fällt das
# nicht auf, wenn `alembic upgrade head` dort von Hand VOR `uvicorn` läuft.
# Ein Container muss das selbst können -- deshalb dieser Entrypoint statt eines
# direkten `CMD uvicorn ...`.
set -eu

# Neue Dateien und Ordner sind nur für den Besitzer lesbar (Datenbank, Dokumente, Zwischendateien): Wird /app/data
# als Ordner vom Host eingebunden, kommen andere Benutzer dieses Rechners so nicht an die Daten. Die Einstellung
# gilt für alles, was dieses Skript startet (Migration, Anwendung, Notseite), auch nach dem Wechsel zu `lattice`.
umask 077

# --- Besitzrechte am Datenordner, dann vom Benutzer root zu `lattice` wechseln ----------------------
#
# Das Image startet als root (kein `USER` im Dockerfile), damit dieses Skript den Datenordner
# /app/data in Ordnung bringen kann: Ein eingebundener NAS-/Host-Ordner gehört oft root oder
# einem anderen Benutzer, dann könnte `lattice` (UID 1000) dort nichts schreiben ("unable to open
# database file"). Danach startet dieses Skript sich SELBST als `lattice` neu (setpriv, Teil von
# util-linux, im Basis-Image vorhanden) -- Migration und Anwendung laufen also nie als root.
#
# Sicherheit:
#  - chown NUR auf /app/data (fest eingetragen, nicht aus einer Umgebungsvariable), und nur auf
#    das, was dort nicht schon `lattice` gehört. `find` folgt keinen Links und bleibt mit -xdev auf
#    dem Dateisystem des Ordners; `chown -h` ändert bei einem Link den Link selbst, nie sein Ziel.
#    Ein Link aus dem Datenordner heraus kann root also nicht auf andere Dateien lenken.
#  - Der Wechsel geschieht VOR `python -m nodvard_deck.boot` und vor dem `exec` der Anwendung.
#    setpriv setzt Benutzer und Gruppe, leert die Zusatzgruppen und verbietet neue Rechte
#    (no-new-privs, z. B. durch setuid-Programme).
#  - Klappt der Wechsel nicht (z. B. wegen `cap_drop: ALL`), bricht das Skript ab, statt als root
#    weiterzulaufen. Dasselbe, wenn es nach dem Wechsel noch root wäre.
#  - Läuft der Container schon als Nicht-root (`user:` in Compose, Kubernetes), wird nichts
#    geändert: kein chown, kein Wechsel, Verhalten wie bisher.
if [ "$(id -u)" = "0" ]; then
  # Nummern des Image-Benutzers; der Rückfall 1000 ist die feste UID/GID aus dem Dockerfile.
  uid="$(id -u lattice 2>/dev/null || echo 1000)"
  gid="$(id -g lattice 2>/dev/null || echo 1000)"
  case "$uid$gid" in
    *[!0-9]*) echo "FEHLER: ungueltige Benutzernummer fuer lattice ($uid:$gid)." >&2; exit 1 ;;
  esac
  if [ "$uid" = "0" ] || [ "$gid" = "0" ]; then
    echo "FEHLER: Der Benutzer lattice darf nicht root sein ($uid:$gid)." >&2
    exit 1
  fi
  if [ "${NODVARD_DECK_PRIV_DROPPED:-}" = "1" ]; then
    echo "FEHLER: Der Wechsel vom Benutzer root zu lattice hat nicht gewirkt; ich laufe nicht als root weiter." >&2
    exit 1
  fi

  if [ -d /app/data ]; then
    find /app/data -xdev \( ! -user "$uid" -o ! -group "$gid" \) -exec chown -h "$uid:$gid" {} + || {
      echo "Warnung: Die Besitzrechte an /app/data liessen sich nicht setzen (NAS mit eingeschraenkten Rechten?)." >&2
      echo "         Gib dem Benutzer mit der Nummer $uid Schreibrechte auf den Ordner, oder starte den Container mit user: \"$uid:$gid\"." >&2
    }
  fi

  # Probelauf: Wechsel möglich? Sonst klar abbrechen, nie als root weitermachen.
  if ! setpriv --reuid="$uid" --regid="$gid" --clear-groups --no-new-privs true; then
    echo "FEHLER: Der Wechsel zum Benutzer lattice ist nicht moeglich (fehlen dem Container Rechte, z. B. durch cap_drop?)." >&2
    echo "        Starte den Container stattdessen direkt als dieser Benutzer: in der Compose-Datei user: \"$uid:$gid\" eintragen" >&2
    echo "        (und dem Ordner /app/data selbst diesem Benutzer zuweisen)." >&2
    exit 1
  fi
  # HOME passend zum neuen Benutzer: sonst erbt der Prozess HOME=/root, und Bibliotheken, die in ~ nachsehen
  # (asyncssh prüft z. B. ~/.ssh/crt), scheitern als lattice am fehlenden Leserecht auf /root -- jede
  # SSH-Verbindung brach dann mit "Permission denied: '/root/.ssh/crt'" ab. Ohne eigenes Verzeichnis: /app.
  home="$(getent passwd "$uid" 2>/dev/null | cut -d: -f6)"
  [ -n "$home" ] && [ -d "$home" ] || home=/app
  export HOME="$home"
  export NODVARD_DECK_PRIV_DROPPED=1
  exec setpriv --reuid="$uid" --regid="$gid" --clear-groups --no-new-privs "$0" "$@"
fi

cd /app

# --- Rechte im Datenordner (jetzt als `lattice`, nicht mehr als root) -------------------------------------------------
#
# Ältere Installationen haben Datenbank und Dokumente mit offenen Rechten (0644/0755) angelegt. Hier werden sie einmal
# je Start geschlossen: nur der Besitzer behält Rechte. Das geschieht bewusst NACH dem Wechsel zu `lattice`: ein
# Programm, das als `lattice` läuft, kann nur Dateien ändern, die ihm gehören, und einem Link, den jemand zwischen
# Suche und Änderung unterschiebt, kann kein root-Prozess mehr zum Ziel gemacht werden. `find` folgt keinen Links
# (`! -type l`) und bleibt mit -xdev auf dem Dateisystem des Ordners. Klappt etwas nicht (z. B. NAS ohne Rechte-Unterstützung),
# gibt es nur eine Warnung.
if [ -d /app/data ] && [ ! -L /app/data ]; then
  chmod 700 /app/data || echo "Warnung: Die Rechte am Ordner /app/data liessen sich nicht auf 0700 setzen." >&2
  find /app/data -xdev ! -type l -perm /077 -exec chmod go-rwx {} + || {
    echo "Warnung: Nicht alle Dateien in /app/data liessen sich auf nur-Besitzer-Rechte setzen (NAS ohne Rechte-Unterstuetzung?)." >&2
  }
fi

# --- Start: erst `nodvard_deck.boot`, scheitert das, die Notseite -- nie eine Neustart-Schleife --------------------------
#
# `python -m nodvard_deck.boot` spielt zuerst eine vorgemerkte Wiederherstellung ein (Einstellungen -> System ->
# Wiederherstellen, der Assistent oder `python -m nodvard_deck.admin restore-backup`), legt dann vor jeder Migration
# eine Kopie der Datenbank an und führt die Migration aus ("alembic upgrade heads": Kern UND jede Erweiterung mit
# eigenem `migrations/versions`-Ordner, siehe nodvard_deck/migrate.py). Beides läuft schon als Benutzer `lattice` (siehe oben).
#
# Früher brach `set -e` hier den Start ab; mit einer Neustart-Regel (`restart: unless-stopped`) startete Docker den
# Container dann endlos neu -- und jedes Mal lief die gescheiterte Migration wieder an. Jetzt:
#   * boot gelingt (0)           -> die Anwendung startet (`exec "$@"`).
#   * boot meldet 75             -> der Datenordner gehört einem anderen Prozess (die laufende Anwendung, z. B. bei einem
#                                   `compose run` neben dem laufenden Container). Es wurde nichts verändert: einfach
#                                   mit 75 beenden, KEINE Notseite.
#   * boot scheitert (sonst)     -> `python -m nodvard_deck.rescue`: die Notseite (nur Standardbibliothek) antwortet auf dem
#                                   Port der Anwendung, `/api/v1/health` mit 503. Sie ersetzt die Anwendung, bis jemand
#                                   mit dem Notfallcode "neu versuchen" wählt (Ende mit 75: der Container endet und
#                                   startet per Restart-Regel neu).
#   * Notseite fällt aus        -> der Container wartet (`exec sleep`), er beendet sich NICHT: eine Schleife aus
#                                   Absturz und Neustart wäre schlimmer als ein stehender Container, den man am
#                                   Protokoll erkennt.
# Die Notseite läuft als Kind (nicht per `exec`), damit dieses Skript bei Nichtstarten noch auf `sleep` ausweichen kann;
# SIGTERM (`docker stop`) gibt es an sie weiter.
boot_rc=0
python -m nodvard_deck.boot || boot_rc=$?
if [ "$boot_rc" -ne 0 ]; then
  if [ "$boot_rc" -eq 75 ]; then
    echo "Der Datenordner wird gerade von einem anderen Prozess benutzt (laeuft Nodvard Deck schon mit diesem Datenordner?). Es wurde nichts veraendert." >&2
    exit 75
  fi
  echo "FEHLER: Der Start ist gescheitert (Code $boot_rc). Statt der Anwendung laeuft jetzt die Notseite; der Notfallcode steht weiter unten im Protokoll." >&2

  # Port und Adresse der Anwendung aus dem Startbefehl (`uvicorn ... --host H --port P`, sonst wie uvicorn `UVICORN_HOST`);
  # nur Zahlen bzw. Adresszeichen -- nie wird etwas aus den Argumenten ausgeführt. Fehlt die Adresse oder ist sie nicht
  # erkennbar (Hostname), lauscht die Notseite nur lokal (127.0.0.1, wie uvicorn ohne --host): lieber zu eng als ins
  # ganze Netz, wenn die Anwendung absichtlich nur lokal lief. Das Image startet mit `--host 0.0.0.0`.
  rescue_host="${UVICORN_HOST:-127.0.0.1}"
  rescue_port=8080
  prev=""
  for arg in "$@"; do
    case "$prev" in
      --port) rescue_port="$arg" ;;
      --host) rescue_host="$arg" ;;
    esac
    case "$arg" in
      --port=*) rescue_port="${arg#--port=}" ;;
      --host=*) rescue_host="${arg#--host=}" ;;
    esac
    prev="$arg"
  done
  case "$rescue_port" in ''|*[!0-9]*) rescue_port=8080 ;; esac
  case "$rescue_host" in ''|*[!0-9A-Fa-f:.]*) rescue_host=127.0.0.1 ;; esac

  NODVARD_DECK_BOOT_EXIT="$boot_rc"
  export NODVARD_DECK_BOOT_EXIT
  python -m nodvard_deck.rescue --host "$rescue_host" --port "$rescue_port" &
  rescue_pid=$!
  trap 'kill -TERM "$rescue_pid" 2>/dev/null || true' TERM INT HUP
  rescue_rc=0
  while :; do
    # `wait` kehrt auch bei einem Signal zurück: dann nachsehen, ob die Notseite wirklich beendet ist.
    wait "$rescue_pid" && rescue_rc=0 || rescue_rc=$?
    kill -0 "$rescue_pid" 2>/dev/null || break
  done
  trap - TERM INT HUP
  case "$rescue_rc" in
    75) exit 75 ;;                    # "neu versuchen": Container endet, die Restart-Regel startet ihn neu
    0|129|130|143) exit 0 ;;          # ordentlich beendet (docker stop)
  esac
  echo "Notseite nicht startbar (Code $rescue_rc) -- der Container bleibt stehen statt in eine Neustart-Schleife zu laufen." >&2
  echo "Die Ursache steht im Protokoll weiter oben. Zum Neu-Versuch den Container von Hand neu starten." >&2
  exec sleep 2147483647
fi

exec "$@"