deploy/entrypoint.sh
Rama main · réplica de lectura del repositorio original.
#!/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 "$@"