diff --git a/docs/install/INSTALL_SCRIPT_ANALYSIS.md b/docs/install/INSTALL_SCRIPT_ANALYSIS.md new file mode 100644 index 0000000..1f069bf --- /dev/null +++ b/docs/install/INSTALL_SCRIPT_ANALYSIS.md @@ -0,0 +1,1106 @@ +# Install.sh - Umfassende Dokumentation + +**Datei:** `install.sh` (695 Zeilen) +**Zweck:** Vollautomatische Installation und Konfiguration des Container Spawner Systems +**Kompatibilität:** Bash, BusyBox (Synology NAS), Docker ≥ 20.10, Docker Compose ≥ 2.0 + +--- + +## Übersicht der Phasen + +Das Script läuft **11 sequenzielle Phasen** durch: + +1. **Startup & Konfiguration** - Logging, Farben, Mindestversionen definieren +2. **.env Prüfung** - Konfigurationsdatei vorhanden? +3. **Voraussetzungen prüfen** - Docker, Compose, Git Versionen validieren +4. **Update vs. Neuinstallation** - Git Repository klonen oder aktualisieren +5. **Verzeichnisse & Rechte** - data/, logs/ erstellen, Berechtigungen setzen +6. **Docker-Netzwerk** - Traefik-Netzwerk erstellen/prüfen +7. **Traefik Prüfung** - Ist Traefik-Container laufend? +8. **Docker Images bauen** - User-Templates und Spawner-Images kompilieren +9. **Container starten** - Docker Compose up, Health-Checks +10. **Fertig-Nachricht** - URLs anzeigen, nützliche Befehle listen + +--- + +## Phase 1: Startup & Konfiguration (Zeilen 1-25) + +### Zweck +Initialisierung des Scripts, Definition globaler Variablen und Logging-Setup. + +### Zeilen 1-2: Shebang & Error Handling +```bash +#!/bin/bash +set -e +``` +- `#!/bin/bash` - Bash-Interpreter aufrufen (nicht sh/dash/ash) +- `set -e` - **KRITISCH**: Beende Script sofort bei erstem Fehler (exit code ≠ 0) +- Verhindert, dass fehlerhafte Befehle ignoriert werden und weitere Schritte ausgeführt werden + +### Zeilen 4-13: Repositoriums- & Installationsvariablen +```bash +REPO_URL="https://gitea.iotxs.de/RainerWieland/spawner.git" +RAW_URL="https://gitea.iotxs.de/RainerWieland/spawner/raw/branch/main" +INSTALL_DIR="${PWD}" +VERSION="0.1.0" +LOG_FILE="${INSTALL_DIR}/spawner-install.log" +``` +- `REPO_URL` - Git Repository für klonen/pull (https, kein SSH) +- `RAW_URL` - Rohe Datei-Downloads (für .env.example) +- `INSTALL_DIR="${PWD}"` - **WICHTIG**: Installiert im aktuellen Verzeichnis (not hardcoded) +- `VERSION` - Script-Version (für Logs) +- `LOG_FILE` - Alle Build-Logs werden hier gesammelt + +**Voraussetzungen:** +- Bash >= 4.0 +- PWD muss beschreibbar sein (chmod 755 mindestens) + +### Zeilen 15-20: Farben für Terminal-Output +```bash +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color +``` +- ANSI Escape Codes für farbige Konsolen-Ausgabe +- Hilft bei Fehler/Success-Anzeige +- Kompatibel mit BusyBox (einfache Codes) + +### Zeilen 22-24: Mindestversionen +```bash +MIN_DOCKER_VERSION="20.10" +MIN_COMPOSE_VERSION="2.0" +``` +- Definieren Minimum-Versionen für Voraussetzungs-Check +- Docker < 20.10: Kein Docker Compose v2 Support +- Compose < 2.0: Alte `docker-compose` CLI (veraltet) + +**Voraussetzungen:** +- Docker 20.10+ installiert +- Docker Compose 2.0+ installiert (integriert in Docker Desktop oder separat) + +--- + +## Phase 2: Version-Vergleich Hilfsfunktion (Zeilen 30-72) + +### Zweck +BusyBox-kompatible Versionierung (Synology NAS nutzt BusyBox, kein `sort -V` vorhanden). + +### Zeilen 30-35: Funktions-Definition +```bash +version_gte() { + local ver1="$1" + local ver2="$2" +``` +- `version_gte "20.10.21" "20.10"` → return 0 (true, >= erfüllt) +- `version_gte "20.0" "20.10"` → return 1 (false, < Minimum) +- **Warum**: `sort -V` existiert in BusyBox nicht + +### Zeilen 40-50: Major/Minor/Patch Parsing +```bash +v1_major=$(echo "$ver1" | cut -d. -f1) +v1_minor=$(echo "$ver1" | cut -d. -f2) +v1_patch=$(echo "$ver1" | cut -d. -f3) +``` +- Teile Version "20.10.21" auf: + - v1_major=20, v1_minor=10, v1_patch=21 + - Verwende 0 als Default wenn Feld fehlt (`${v1_minor:-0}`) + +### Zeilen 52-71: Vergleich Logic +1. **Major vergleichen** (Zeile 53-57) + - Wenn v1_major > v2_major → return 0 (erfüllt) + - Wenn v1_major < v2_major → return 1 (nicht erfüllt) + - Wenn gleich → weiter zu Minor + +2. **Minor vergleichen** (Zeile 60-63) + - Analog Major-Vergleich + +3. **Patch vergleichen** (Zeile 67-70) + - `version_patch >= version_patch` → return 0 + +**Beispiele:** +- `version_gte "20.10.21" "20.10"` → 20==20, 10==10, 21>=0 → **TRUE** +- `version_gte "20.9.0" "20.10"` → 20==20, 9<10 → **FALSE** +- `version_gte "21.0" "20.10"` → 21>20 → **TRUE** + +**Voraussetzungen:** +- cut, echo Commands vorhanden (in BusyBox) + +--- + +## Phase 3: .env Datei Prüfung (Zeilen 74-115) + +### Zweck +Prüfe ob .env existiert. Wenn nicht → Download .env.example und beende Script. + +### Zeilen 74-82: Willkommens-Banner & Log-Init +```bash +echo "============================================================" +echo " Container Spawner Installation v${VERSION}" +echo "============================================================" +``` +- Zeige Script-Version an +- Initialisiere Log-Datei mit Timestamp + +### Zeilen 87-100: .env Existenz-Check +```bash +if [ ! -f "${INSTALL_DIR}/.env" ]; then + echo "Lade .env.example herunter..." + if command -v curl >/dev/null 2>&1; then + curl -sSL "${RAW_URL}/.env.example" -o "${INSTALL_DIR}/.env.example" + elif command -v wget >/dev/null 2>&1; then + wget -q "${RAW_URL}/.env.example" -O "${INSTALL_DIR}/.env.example" +``` + +**Logik:** +- Existiert `.env` NICHT? + 1. Versuche `.env.example` herunterzuladen (über curl oder wget) + 2. Speichere in `${INSTALL_DIR}/.env.example` + 3. **Stoppe Script mit exit 0** → Zeige Anleitung zur manuellen Konfiguration + +**Warum .env nicht automatisch kopieren?** +- `.env` enthält Secrets (SECRET_KEY, SMTP_PASSWORD etc.) +- Admin muss Werte bewusst setzen (BASE_DOMAIN, TRAEFIK_NETWORK etc.) +- Verhindert Sicherheitslücken durch Auto-Configuration + +### Zeilen 104-111: Konfigurationsanleitung +```bash +echo "Naechste Schritte:" +echo " 1. Kopiere die Vorlage: cp .env.example .env" +echo " 2. Passe die Werte an: nano .env" +echo " 3. Fuehre erneut aus: bash install.sh" +``` + +**Voraussetzungen:** +- curl ODER wget installiert +- Internet-Zugriff zu Gitea-Server +- `.env.example` im Repository vorhanden + +--- + +## Phase 4: Voraussetzungen Prüfung (Zeilen 122-185) + +### Zweck +Validiere dass alle erforderlichen Tools verfügbar und aktuelle Versionen sind. + +### Zeilen 128-149: Docker Version Check +```bash +if ! command -v docker >/dev/null 2>&1; then + echo -e "${RED}Fehler: Docker nicht gefunden!${NC}" + exit 1 +fi + +DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null || \ + docker version 2>/dev/null | grep -i "version" | head -1 | sed 's/.*version[: ]*\([0-9.]*\).*/\1/') +``` + +**Logic:** +1. Prüfe ob `docker` command existiert + - `command -v` ist POSIX-kompatibel (funktioniert auch in BusyBox/ash) +2. Extrahiere Docker Server-Version: + - Versuche `docker version --format` (moderne Syntax) + - Fallback zu regex parsing (für ältere Docker Versionen) +3. Vergleiche gegen MIN_DOCKER_VERSION="20.10" mit `version_gte()` Funktion + +**Fehlerbehandlung:** +```bash +if [ -z "$DOCKER_VERSION" ]; then + echo "Version unbekannt (OK, mit Warnung fortfahren)" +elif version_gte "$DOCKER_VERSION" "$MIN_DOCKER_VERSION"; then + echo "OK (v${DOCKER_VERSION})" +else + echo "FEHLER: Version ${DOCKER_VERSION} zu alt" + exit 1 +fi +``` + +**Voraussetzungen:** +- `docker` executable vorhanden (PATH) +- Docker Server läuft (für version --format) + +### Zeilen 151-178: Docker Compose Check +```bash +if docker compose version >/dev/null 2>&1; then + COMPOSE_CMD="docker compose" # Neue Syntax (v2) +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD="docker-compose" # Alte Syntax (v1) +else + echo -e "${RED}Fehler: Docker Compose nicht gefunden!${NC}" + exit 1 +fi +``` + +**Logic:** +1. Prüfe ob `docker compose` vorhanden (Docker Desktop oder separate Installation) +2. Fallback zu `docker-compose` (veraltet aber noch unterstützt) +3. Speichere in `COMPOSE_CMD` Variable für später + +**Wichtig:** `${COMPOSE_CMD}` wird später überall verwendet (Zeile 371, 626 etc.) + +**Voraussetzungen:** +- Docker Compose v2+ oder `docker-compose` command vorhanden + +### Zeilen 180-185: Git Check +```bash +if ! command -v git >/dev/null 2>&1; then + echo -e "${RED}Fehler: Git nicht gefunden!${NC}" + exit 1 +fi +``` + +**Benötigt für:** +- Clone Repository bei Neuinstallation (Zeile 220) +- Pull Updates bei bestehender Installation (Zeile 209) +- Versionskontrolle + +**Voraussetzungen:** +- `git` executable vorhanden (mindestens Git 2.0) + +--- + +## Phase 5: Update vs. Neuinstallation (Zeilen 188-241) + +### Zweck +Prüfe ob bereits Installation existiert. Wenn ja → update via git pull. Wenn nein → clone repository. + +### Zeilen 192-193: Git Safe Directory (Synology-Workaround) +```bash +git config --global --add safe.directory "${INSTALL_DIR}" 2>/dev/null || true +``` + +**Warum?** +- Synology NAS: Docker Container läuft oft als root, Host-Verzeichnis ist root-owned +- Git misstraut Verzeichnissen mit unterschiedlichem Owner +- Fehler: `fatal: detected dubious ownership in repository at ...` +- Lösung: `safe.directory` erlaubt Repository trotzdem + +**Syntax:** +- `2>/dev/null || true` - Ignoriere Fehler (akzeptabel wenn git zu alt) + +### Zeilen 195-213: Update-Branch (Existierende Installation) +```bash +if [ -d "${INSTALL_DIR}/.git" ]; then + echo "Update erkannt - hole neueste Aenderungen..." + cd "${INSTALL_DIR}" + + # Sichere lokale Aenderungen + if git diff --quiet 2>/dev/null; then + : # Keine Aenderungen + else + git stash 2>/dev/null || true + fi + + # Update durchfuehren + if git fetch origin && git pull origin main 2>/dev/null; then + echo "Repository aktualisiert" + else + echo "Git-Update fehlgeschlagen, fahre mit lokalen Dateien fort..." + fi +``` + +**Logik:** +1. Existiert `.git` Verzeichnis? → Installation vorhanden +2. Prüfe auf lokale Änderungen mit `git diff --quiet` +3. Falls Änderungen → `git stash` (temporär speichern) +4. `git fetch` + `git pull origin main` (Update durchführen) +5. Falls fehlgeschlagen → Warnung aber nicht abbrechen (fahre mit lokalen Dateien fort) + +**Warum stash?** +- User hat möglicherweise Dateien lokal angepasst (z.B. api.py Bugfixes) +- `git pull` würde MERGE CONFLICT verursachen +- `git stash` speichert lokale Änderungen sicher → Merge ist sauber + +**Voraussetzungen:** +- `.git` Verzeichnis existiert (= Installation bereits vorhanden) +- `origin` Remote existiert (Standard bei clone) + +### Zeilen 214-240: Clone-Branch (Neuinstallation) +```bash +else + echo "Neuinstallation - klone Repository..." + + TEMP_DIR=$(mktemp -d) + + if git clone "${REPO_URL}" "${TEMP_DIR}"; then + shopt -s dotglob 2>/dev/null || true + for item in "${TEMP_DIR}"/*; do + basename_item=$(basename "$item") + if [ "$basename_item" != ".env" ] && [ "$basename_item" != ".git" ]; then + cp -r "$item" "${INSTALL_DIR}/" + fi + done + + cp -r "${TEMP_DIR}/.git" "${INSTALL_DIR}/" + rm -rf "${TEMP_DIR}" +``` + +**Logik:** +1. Keine `.git` Verzeichnis → Neuinstallation +2. Clone in **temporäres Verzeichnis** (nicht direkt nach INSTALL_DIR) +3. Kopiere Dateien (außer `.env` und `.git`) +4. Kopiere `.git` Verzeichnis nachträglich (für zukünftige Updates) +5. Räume Temp-Verzeichnis auf + +**Warum nicht direkt klonen?** +- `git clone` würde `.env` überschreiben (wenn Admin bereits angepasst hat) +- Temporärer Clone als Workaround + +**Voraussetzungen:** +- `git clone` funktioniert (Internet, SSH-Key oder HTTPS) +- mktemp Command vorhanden (POSIX-kompatibel) + +--- + +## Phase 6: Verzeichnisse & Berechtigungen (Zeilen 244-285) + +### Zweck +Erstelle erforderliche Verzeichnisse und setze Berechtigungen für Docker-Zugriff. + +### Zeilen 249-255: Verzeichnisse erstellen +```bash +mkdir -p "${INSTALL_DIR}/data" +mkdir -p "${INSTALL_DIR}/logs" + +chmod 755 "${INSTALL_DIR}/data" +chmod 755 "${INSTALL_DIR}/logs" +``` + +**Warum diese Verzeichnisse?** +- `data/` - Persistente Datenbank + Konfiguration +- `logs/` - Installations- und Anwendungs-Logs + +**Berechtigungen (755 = rwxr-xr-x):** +- Owner: read + write + execute +- Group: read + execute (nur) +- Others: read + execute (nur) + +**Kompatibilität:** +- Docker Container als root: 755 ausreichend +- Docker Container als non-root: Könnten 777 brauchen (kommentiert Zeile 271) + +### Zeilen 262-274: Root-Check +```bash +if [ "$(id -u)" = "0" ]; then + # Als root: 755 reicht + echo "755 (root)" +else + # Als normaler User: 755 auch OK + echo "755" +fi +``` + +**Warum Root-Check?** +- Synology NAS: Installation oft als root +- Docker-Container: Läuft auch als root +- User-Scripts (z.B. cron): Könnten als normaler User laufen + +**Regel:** +- Root + Docker-Root: 755 OK +- Normaler User + Docker-Root: 755 auch OK +- Normaler User + Docker-NonRoot: 777 nötig (siehe Kommentar) + +### Zeilen 277-279: .env schützen +```bash +if [ -f "${INSTALL_DIR}/.env" ]; then + chmod 600 "${INSTALL_DIR}/.env" + echo ".env: OK (600, nur Owner)" +``` + +**Berechtigungen (600 = rw-------):** +- Nur Owner: read + write +- Group/Others: kein Zugriff + +**WICHTIG:** Secrets in .env: +- `SECRET_KEY` - Flask Session Secret +- `SMTP_PASSWORD` - Email-Authentifizierung +- Darf nicht für alle lesbar sein! + +### Zeilen 282-284: install.sh auführbar machen +```bash +if [ -f "${INSTALL_DIR}/install.sh" ]; then + chmod +x "${INSTALL_DIR}/install.sh" +``` + +**Warum?** +- Nach git pull/clone könnte execute-bit verloren gehen +- Stelle sicher dass `bash install.sh` immer funktioniert + +**Voraussetzungen:** +- chmod Command vorhanden (POSIX) +- Berechtigungen änderbar (nicht in read-only Filesystem) + +--- + +## Phase 7: Docker-Netzwerk Prüfung (Zeilen 288-309) + +### Zweck +Prüfe dass Traefik-Netzwerk existiert. Wenn nicht → Erstelle automatisch. + +### Zeilen 291-292: Netzwerk aus .env lesen +```bash +NETWORK="${TRAEFIK_NETWORK:-web}" +echo "Pruefe Docker-Netzwerk: ${NETWORK}" +``` + +- Lese `TRAEFIK_NETWORK` Variable aus .env +- Fallback zu "web" wenn nicht definiert + +### Zeilen 294-309: Netzwerk-Logik +```bash +if docker network inspect "${NETWORK}" >/dev/null 2>&1; then + echo "Netzwerk '${NETWORK}': existiert" +else + echo "Fehler: Docker-Netzwerk '${NETWORK}' existiert nicht!" + + # Automatische Erstellung (vorher: interaktive Frage) + docker network create "${NETWORK}" 2>/dev/null || true + echo "Netzwerk '${NETWORK}': erstellt/vorhanden" +fi +``` + +**Logik:** +1. Prüfe ob Netzwerk existiert mit `docker network inspect` + - Erfolgreich → Netzwerk vorhanden, weitermachen + - Fehler → Netzwerk nicht vorhanden + +2. **Automatische Erstellung** (Zeile 307) + - Erstelle Netzwerk: `docker network create "${NETWORK}"` + - `2>/dev/null || true` - Ignoriere Fehler (z.B. bereits vorhanden) + - Dadurch war das Script nicht mehr blockiert (früher: `read -p "...?"`) + +**Warum dieses Netzwerk?** +- Traefik + Spawner müssen im **gleichen Netzwerk** sein +- Nur so können Traefik die User-Container entdecken und routen +- Name definiert in `.env` → `TRAEFIK_NETWORK=web` (oder custom) + +**Voraussetzungen:** +- Docker Daemon läuft +- Benutzer hat docker Zugriff (sudo oder docker-Gruppe) +- Netzwerk nicht durch andere Tools belegt + +--- + +## Phase 8: Traefik Prüfung (Zeilen 312-361) + +### Zweck +Prüfe ob Traefik-Container läuft. Falls ja → Validiere dass es im richtigen Netzwerk ist. + +### Zeilen 318-333: Traefik-Container Suche (3 Versuche) + +**Versuch 1 (Zeile 318): Nach Name suchen** +```bash +TRAEFIK_CONTAINER=$(docker ps --filter "name=traefik" --filter "status=running" \ + --format "{{.Names}}" 2>/dev/null | head -1) +``` +- Suche nach Container mit "traefik" im Namen +- `--filter "status=running"` - Nur laufende Container +- `--format "{{.Names}}"` - Nur Container-Name ausgeben +- `head -1` - Nimm ersten (falls mehrere) + +**Versuch 2 (Zeile 322): Nach Image suchen** +```bash +if [ -z "$TRAEFIK_CONTAINER" ]; then + TRAEFIK_CONTAINER=$(docker ps --filter "ancestor=traefik" \ + --filter "status=running" --format "{{.Names}}" 2>/dev/null | head -1) +fi +``` +- Falls Name-Suche leer → Suche nach `ancestor=traefik` (Image-Name) +- Findungschance: Container könnte anders benannt sein, aber vom traefik Image stammen + +**Versuch 3 (Zeile 327-332): Nach Label suchen** +```bash +if [ -z "$TRAEFIK_CONTAINER" ]; then + TRAEFIK_CONTAINER=$(docker ps ... | while read name; do + if docker inspect "$name" 2>/dev/null | grep -q '"traefik.http.routers\|com.docker.compose.service": "traefik"'; then + echo "$name" + break + fi + done) +fi +``` +- Falls noch immer nicht gefunden → Prüfe Labels +- Traefik setzt typischerweise: `com.docker.compose.service=traefik` + +### Zeilen 335-361: Traefik-Status Ausgabe + +**Falls gefunden (Zeile 335-350):** +```bash +if [ -n "$TRAEFIK_CONTAINER" ]; then + TRAEFIK_VERSION=$(docker exec "$TRAEFIK_CONTAINER" traefik version 2>/dev/null | ...) + echo "Traefik: OK (Container: ${TRAEFIK_CONTAINER}, v${TRAEFIK_VERSION})" + + # Prüfe ob im richtigen Netzwerk + TRAEFIK_NETWORKS=$(docker inspect "$TRAEFIK_CONTAINER" \ + --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}') + if echo "$TRAEFIK_NETWORKS" | grep -q "$NETWORK"; then + echo "Traefik-Netzwerk: OK (verbunden mit '${NETWORK}')" + else + echo "WARNUNG: Traefik nicht mit Netzwerk '${NETWORK}' verbunden" + echo "Traefik-Netzwerke: ${TRAEFIK_NETWORKS}" + fi +fi +``` + +**Checks:** +1. `traefik version` - Zeige Traefik-Version an +2. `docker inspect` - Prüfe Netzwerk-Verbindungen +3. Regex-Match gegen `${NETWORK}` - Sind Traefik und Spawner im gleichen Netzwerk? + +**Falls nicht gefunden (Zeile 352-361):** +```bash +else + echo "WARNUNG: Kein laufender Traefik-Container gefunden!" + echo "Traefik wird fuer das Routing benoetigt, aber Script faehrt fort..." +``` + +- **WARNUNG nicht FEHLER** → Script bricht nicht ab +- Spawner funktioniert auch ohne Traefik: + - Nur lokal erreichbar (localhost:5000) + - Keine automatischen Subdomains + - Keine User-Container Routing + +**Voraussetzungen:** +- Docker Daemon läuft und antwortet +- Traefik-Container läuft (optional, mit Warnung weitergemacht) + +--- + +## Phase 9: Docker Images Bauen (Zeilen 364-563) + +### Zweck +Baue alle erforderlichen Docker Images: +1. **User-Templates** (template-01, template-02, template-next etc.) +2. **Spawner API** (Flask Backend) +3. **Spawner Frontend** (Next.js) + +### Zeilen 370-378: USER_TEMPLATE_IMAGES aus .env auslesen +```bash +USER_TEMPLATE_IMAGES="" +if [ -f "${INSTALL_DIR}/.env" ]; then + USER_TEMPLATE_IMAGES=$(grep "^USER_TEMPLATE_IMAGES=" "${INSTALL_DIR}/.env" | \ + cut -d'=' -f2- | tr -d '"' | tr -d "'") +fi +``` + +**Parse-Logic:** +1. `grep "^USER_TEMPLATE_IMAGES="` - Finde genau diese Zeile (nicht commented) +2. `cut -d'=' -f2-` - Alles nach dem `=` Zeichen +3. `tr -d '"' | tr -d "'"` - Entferne Quotes (USER_TEMPLATE_IMAGES="...;...;...") + +**Beispiel:** +```bash +# In .env +USER_TEMPLATE_IMAGES="user-template-01:latest;user-template-02:latest;user-template-next:latest" + +# Nach Parsing +USER_TEMPLATE_IMAGES="user-template-01:latest;user-template-02:latest;user-template-next:latest" +``` + +### Zeilen 380-385: Fallback auf .env.example +```bash +if [ -z "$USER_TEMPLATE_IMAGES" ] && [ -f "${INSTALL_DIR}/.env.example" ]; then + echo "⚠️ USER_TEMPLATE_IMAGES nicht definiert" + echo " Nutze .env.example als Fallback..." + USER_TEMPLATE_IMAGES=$(grep "^USER_TEMPLATE_IMAGES=" "${INSTALL_DIR}/.env.example" | ...) +fi +``` + +- Wenn `.env` USER_TEMPLATE_IMAGES nicht hat → Versuche `.env.example` +- Erlaubt Test-Installation ohne .env Anpassung + +### Zeilen 387-437: Fallback-Modus (Keine .env Konfiguration) +```bash +if [ -z "$USER_TEMPLATE_IMAGES" ]; then + echo "⚠️ USER_TEMPLATE_IMAGES nicht konfiguriert" + echo " Fallback: Baue alle user-template-* Verzeichnisse..." + + for template_dir in "${INSTALL_DIR}"/user-template*; do + [ -d "$template_dir" ] || continue + template_name=$(basename "$template_dir") + image_name="${template_name}:latest" + + docker build --no-cache -t "${image_name}" "${template_dir}/" >> "${BUILD_LOG}" 2>&1 +``` + +**Logik (Rückwärtskompatibilität):** +- Wenn USER_TEMPLATE_IMAGES leer → Auto-Detection +- Suche alle `user-template-*` Verzeichnisse +- Baue jedes als Docker Image + +**Beispiel:** +``` +user-template-01/ +user-template-02/ +user-template-next/ + +→ Baue: user-template-01:latest, user-template-02:latest, user-template-next:latest +``` + +**Warum Fallback?** +- Alte Installationen ohne .env Konfiguration müssen auch funktionieren +- Neue Installationen verwenden .env-basiertes System + +### Zeilen 440-562: .env-basiertes Building (NEUE LOGIK) +```bash +else + echo " Baue Templates aus .env Konfiguration..." + + # Split by Semicolon + IFS=';' read -ra TEMPLATE_IMAGES <<< "$USER_TEMPLATE_IMAGES" + + TOTAL_TEMPLATES=${#TEMPLATE_IMAGES[@]} + + for image_with_tag in "${TEMPLATE_IMAGES[@]}"; do + image_with_tag=$(echo "$image_with_tag" | xargs) # Trim whitespace + [ -z "$image_with_tag" ] && continue + + # Extract directory name (vor dem :) + template_dir_name="${image_with_tag%%:*}" + + # Extract tag (nach dem :) + template_tag="${image_with_tag##*:}" + [ -z "$template_tag" ] && template_tag="latest" + + template_dir="${INSTALL_DIR}/${template_dir_name}" +``` + +**Parse-Logik für "user-template-01:latest":** +1. Split Array by `;` + - `"user-template-01:latest;user-template-02:latest"` + - → `["user-template-01:latest", "user-template-02:latest"]` + +2. Trim Whitespace mit `xargs` + - `" user-template-01:latest "` → `"user-template-01:latest"` + +3. Extrahiere Image-Name und Tag + - `${image%%:*}` - Alles VOR dem ersten `:` (user-template-01) + - `${image##*:}` - Alles NACH dem letzten `:` (latest) + +**Validation (Zeilen 475-493):** +```bash +# Prüfe ob Verzeichnis existiert +if [ ! -d "$template_dir" ]; then + echo "❌ Fehler: Template-Verzeichnis nicht gefunden" + echo " Definiert in .env: USER_TEMPLATE_IMAGES" + echo " Erwartetes Verzeichnis: ${template_dir}" + continue # Überspringe dieses Template +fi + +# Dockerfile vorhanden? +if [ ! -f "${template_dir}/Dockerfile" ]; then + echo "❌ Fehler: Kein Dockerfile gefunden" + continue +fi +``` + +- Nicht-existente Verzeichnisse → WARNUNG + continue (nicht FEHLER) +- So können einzelne Templates fehlschlagen, andere funktionieren weiterhin + +**Build-Logik (Zeilen 507-527):** +```bash +docker build --no-cache -t "${template_dir_name}:${template_tag}" "${template_dir}/" >> "${BUILD_LOG}" 2>&1 +BUILD_EXIT=$? + +# Zeige gefilterten Output +grep -E "(Step |#[0-9]+ |Successfully|ERROR|error:|COPY|RUN|FROM)" "${BUILD_LOG}" | sed 's/^/ /' + +# Prüfe ob erfolgreich +if [ $BUILD_EXIT -eq 0 ] && docker image inspect "${template_dir_name}:${template_tag}" >/dev/null 2>&1; then + echo "✅ ${template_name}: OK" + BUILT_TEMPLATES=$((BUILT_TEMPLATES + 1)) +else + echo "❌ ${template_name}: FEHLER" + tail -50 "${BUILD_LOG}" + exit 1 # Breche ab +fi +``` + +**Build-Logik:** +1. Führe `docker build` aus, leite Output zu Log-Datei +2. Zeige gefilterte Build-Schritte (Step, FROM, RUN, COPY, ERROR) +3. Prüfe ob erfolgreich: + - Exit-Code = 0? + - `docker image inspect` findet das Image? +4. Beide Bedingungen erfüllt → OK +5. Sonst → FEHLER, zeige last 50 Zeilen, beende Script + +**Spezieller Handling für Next.js (Zeile 497-498):** +```bash +if [[ "$template_dir_name" == *"next"* ]]; then + echo "${BLUE}Dies kann 2-5 Minuten dauern (npm install + build)...${NC}" +fi +``` + +- Next.js Templates brauchen länger (npm install + npm run build) +- Warnung damit Admin nicht denkt Installation ist hänggeblieben + +**Ungekonfigurierte Templates Warnung (Zeilen 539-561):** +```bash +echo " Prüfe auf ungekonfigurierte Template-Verzeichnisse..." + +for template_dir in "${INSTALL_DIR}"/user-template*; do + template_name=$(basename "$template_dir") + + # Ist dieses Template in USER_TEMPLATE_IMAGES definiert? + if [[ ! "$USER_TEMPLATE_IMAGES" =~ "$template_name" ]]; then + echo " ⚠️ ${template_name} (nicht in USER_TEMPLATE_IMAGES definiert)" + fi +done +``` + +- Hilft Admin zu sehen, welche Templates **nicht** gebaut wurden +- Z.B. Template existiert lokal, aber nicht in .env → wird ignoriert + +### Zeilen 565-616: Spawner API & Frontend bauen + +**Spawner API (Flask Backend) - Zeilen 565-587:** +```bash +echo " Baue Spawner API (Flask Backend)..." + +docker build --no-cache -t spawner:latest "${INSTALL_DIR}/" >> "${BUILD_LOG}" 2>&1 +BUILD_EXIT=$? + +if [ $BUILD_EXIT -eq 0 ] && docker image inspect spawner:latest >/dev/null 2>&1; then + echo " spawner-api: ${GREEN}OK${NC}" +else + echo " spawner-api: ${RED}FEHLER${NC}" + tail -50 "${BUILD_LOG}" + exit 1 +fi +``` + +- Baut Image aus Dockerfile im **root directory** (nicht frontend/) +- Image-Name: `spawner:latest` +- **Muss erfolgreich sein** (exit 1 bei Fehler) + +**Spawner Frontend (Next.js) - Zeilen 590-616:** +```bash +if [ -d "${INSTALL_DIR}/frontend" ]; then + echo " Baue Frontend (Next.js)..." + echo "${BLUE}Dies kann 2-5 Minuten dauern (npm install + build)...${NC}" + + docker build --no-cache -t spawner-frontend:latest "${INSTALL_DIR}/frontend/" >> "${BUILD_LOG}" 2>&1 + ... +fi +``` + +- **Optional Check**: Falls `frontend/` Verzeichnis nicht vorhanden → überspringe +- Image-Name: `spawner-frontend:latest` +- Warnung dass npm build lange dauert + +**Voraussetzungen:** +- `docker build` Command funktioniert +- Ausreichend Disk-Space für Images +- Internetzugriff (für npm install, pip install etc.) +- Dockerfile(s) syntaktisch korrekt + +--- + +## Phase 10: Container Starten (Zeilen 621-646) + +### Zweck +Starte Docker Compose Services (Spawner API, Frontend, optional Traefik) und prüfe Health. + +### Zeilen 625-626: Compose Up +```bash +echo "" +echo "Starte Container..." +${COMPOSE_CMD} up -d +``` + +- `${COMPOSE_CMD}` ist entweder `docker compose` oder `docker-compose` (aus Phase 4) +- `-d` Flag: Detached Mode (laufen im Hintergrund) + +**Was wird gestartet?** (Definiert in docker-compose.yml) +- spawner (API, Port 5000) +- spawner-frontend (Next.js, Port 3000) +- Optional: traefik, database etc. (je nach Konfiguration) + +### Zeilen 631-646: Health-Checks +```bash +echo "Warte auf Spawner-Start..." +sleep 5 + +SPAWNER_URL="http://localhost:${SPAWNER_PORT:-5000}/health" +if curl -sf "${SPAWNER_URL}" >/dev/null 2>&1; then + echo " API Health-Check: ${GREEN}OK${NC}" +else + echo " API Health-Check: ${YELLOW}Noch nicht bereit (normal beim ersten Start)${NC}" +fi + +if curl -sf "http://localhost:3000/" >/dev/null 2>&1; then + echo " Frontend Health-Check: ${GREEN}OK${NC}" +else + echo " Frontend Health-Check: ${YELLOW}Noch nicht bereit${NC}" +fi +``` + +**Logik:** +1. Warte 5 Sekunden (Container brauchen Zeit zu starten) +2. Prüfe API: `curl -sf http://localhost:5000/health` + - `-s` Silent mode + - `-f` Fail silently auf HTTP-Error +3. Prüfe Frontend: `curl -sf http://localhost:3000/` + +**Status:** +- Beide OK → ✅ Alles funktioniert +- Eine/beide nicht bereit → ⚠️ Normal beim ersten Start (DB wird initialisiert etc.) + +**Voraussetzungen:** +- curl Command verfügbar +- Container haben Zeit zu starten +- Ports 5000 und 3000 lokal verfügbar + +--- + +## Phase 11: Fertig-Nachricht (Zeilen 648-695) + +### Zweck +Zeige Erfolgs-Nachricht und nützliche Informationen für Admin. + +### Zeilen 652-656: Success Banner +```bash +echo "============================================================" +echo -e " ${GREEN}Installation abgeschlossen!${NC}" +echo "============================================================" +echo "" +echo "Installations-Log: ${LOG_FILE}" +``` + +- Zeige Erfolgs-Banner +- Link zu Log-Datei (für Debugging) + +### Zeilen 659-668: URLs anzeigen +```bash +SCHEME="https" +if [ "${BASE_DOMAIN:-localhost}" = "localhost" ]; then + SCHEME="http" +fi +FULL_URL="${SCHEME}://${SPAWNER_SUBDOMAIN:-coder}.${BASE_DOMAIN:-localhost}" + +echo "Zugriff:" +echo " Frontend: ${FULL_URL}" +echo " API: ${FULL_URL}/api" +echo " Health: ${FULL_URL}/health" +``` + +**Logic:** +- Lese `BASE_DOMAIN` und `SPAWNER_SUBDOMAIN` aus .env +- Falls localhost → nutze http:// (kein HTTPS) +- Sonst → nutze https:// (erwartet Let's Encrypt) + +**Beispiel .env:** +``` +BASE_DOMAIN=wieland.org +SPAWNER_SUBDOMAIN=coder +``` + +→ URLs: +- Frontend: https://coder.wieland.org +- API: https://coder.wieland.org/api +- Health: https://coder.wieland.org/health + +### Zeilen 670-678: Lokale URLs & Befehle +```bash +echo "" +echo "Lokaler Zugriff (ohne Traefik):" +echo " API: http://localhost:${SPAWNER_PORT:-5000}" +echo " Frontend: http://localhost:3000" +echo "" +echo "Nützliche Befehle:" +echo " Status: ${COMPOSE_CMD} ps" +echo " Logs API: ${COMPOSE_CMD} logs -f spawner" +echo " Logs FE: ${COMPOSE_CMD} logs -f frontend" +``` + +- Zeige wie man Services ohne Traefik zugreift +- Zeige häufige Wartungs-Befehle + +### Zeilen 681-695: Wichtige Informationen +```bash +echo "WICHTIG - Multi-Container MVP:" +echo " - Jeder User erhält 2 Container: Development und Production" +echo " - Dev Container: https://${SPAWNER_SUBDOMAIN}.${BASE_DOMAIN}/{slug}-dev" +echo " - Prod Container: https://${SPAWNER_SUBDOMAIN}.${BASE_DOMAIN}/{slug}-prod" +echo "" +echo "WICHTIG - Passwordless Auth:" +echo " Das System nutzt Magic Links (Email-basiert)!" +echo " - SMTP konfigurieren: .env Datei anpassen" +echo " - Datenbank wird automatisch mit allen Tabellen erstellt" +``` + +- Erkläre Multi-Container Feature +- Erkläre Passwordless Authentication +- Hinweis auf SMTP Konfiguration + +--- + +## Zusammenfassung der Abhängigkeiten + +### Externe Tools (müssen vorhanden sein) +| Tool | Min-Version | Zweck | Fallback | +|------|-------------|-------|---------| +| docker | 20.10 | Container management | Keine | +| docker compose | 2.0 | Service orchestration | docker-compose (v1) | +| git | 2.0+ | Repository management | Keine | +| curl/wget | - | Download .env.example | curl ODER wget | +| bash | 4.0+ | Script interpreter | Nur bash (nicht sh) | +| grep, cut, tr, sed | - | Text parsing | BusyBox kompatibel | + +### Umgebungsvariablen (.env) +| Variable | Beispiel | Zweck | Required | +|----------|---------|-------|----------| +| `SECRET_KEY` | `abc123...` | Flask session secret | Ja | +| `BASE_DOMAIN` | `wieland.org` | Hauptdomain | Ja | +| `SPAWNER_SUBDOMAIN` | `coder` | Subdomain prefix | Ja | +| `TRAEFIK_NETWORK` | `web` | Docker network für Traefik | Ja | +| `USER_TEMPLATE_IMAGES` | `user-template-01:latest;...` | Templates zum bauen | Optional (Fallback) | +| `SPAWNER_PORT` | `5000` | Backend Port | Optional | + +### Dateisystem-Anforderungen +- **Disk-Space**: 3-5 GB (für Docker Images) +- **Verzeichnisse**: + - `.env` - Konfiguration (chmod 600) + - `data/` - Datenbank (chmod 755) + - `logs/` - Installation/App Logs (chmod 755) + - `frontend/` - Frontend Code (optional) + - `user-template-*/` - Template Dockerfiles + +### Netzwerk-Anforderungen +- **Internet**: Zum downloaden .env.example, docker images, npm/pip packages +- **Docker Daemon**: Lokal erreichbar (Unix Socket oder TCP) +- **Docker Network**: `${TRAEFIK_NETWORK}` darf nicht existieren (oder wird geteilt mit Traefik) + +### Sicherheits-Anforderungen +- `.env` muss **before** Installation vorhanden sein (mit SECRET_KEY gesetzt) +- Docker Socket nur für vertrauenswürdige Benutzer zugänglich +- SMTP-Credentials (wenn konfiguriert) müssen sicher sein + +--- + +## Fehlerbehandlung & Logging + +### Log-Datei: `spawner-install.log` + +Alle Build-Outputs werden geloggt: +``` +=== Spawner Installation 2026-02-03 10:00:00 === + +=== Build: user-template-01 === +Step 1/5 : FROM nginx:latest + ---> ... +Step 2/5 : COPY index.html /usr/share/nginx/html/ +... +Successfully built abc123 + +=== Build: spawner-api === +... +``` + +### Fehler-Handling mit `set -e` + +**Reihenfolge:** +1. Script stoppt sofort bei erstem Fehler (`set -e`) +2. Zuletzt erfolgreich durchgeführte Zeile wird gezeigt +3. Admin kann Log-Datei prüfen: `tail spawner-install.log` + +**Beispiel:** +```bash +# Fehler: Docker nicht installiert +$ bash install.sh +Fehler: Docker nicht gefunden! +→ exit 1 (Script stoppt) +→ install.sh log nicht weitergehe +``` + +### Recovery-Strategien + +**Fehler: Zu alte Docker Version** +```bash +→ Installiere Docker ≥ 20.10 +→ Starte install.sh erneut +``` + +**Fehler: .env nicht gefunden** +```bash +→ cp .env.example .env +→ nano .env (setze SECRET_KEY, BASE_DOMAIN etc.) +→ bash install.sh +``` + +**Fehler: Template-Image Build fehlt** +```bash +→ Prüfe docker build Fehler in Log +→ Ursache meist: Dockerfile Syntax, Base Image nicht vorhanden +→ Korrigiere Dockerfile +→ Starte install.sh erneut (re-baut Imagen) +``` + +**Fehler: Git Clone fehlgeschlagen** +```bash +→ Prüfe Internet-Zugriff: curl https://gitea.iotxs.de +→ Prüfe Git SSH/HTTPS Keys (falls SSH) +→ Starte install.sh erneut +``` + +--- + +## Tipps für Synology NAS + +### BusyBox-Kompatibilität + +Install.sh läuft auf Synology mit BusyBox-Shell. Wichtige Unterschiede: + +| Bash | BusyBox | Workaround in install.sh | +|------|---------|-------------------------| +| `sort -V` | Nicht vorhanden | Eigene `version_gte()` Funktion | +| `grep -P` (Perl) | Nicht vorhanden | Nutze Basic `grep` | +| `shopt` | Teilweise | `2>/dev/null \|\| true` (ignore error) | + +### Docker auf Synology + +```bash +# Docker läuft meist als root +id -u # → 0 (root) + +# Berechtigungen daher 755 OK (nicht 777 nötig) + +# Git safe.directory Workaround +git config --global --add safe.directory /volume1/docker/spawner +``` + +### Disk-Space prüfen + +```bash +# Vor Installation +df -h /volume1/docker/ + +# Braucht ~5 GB für alle Images +# Bei < 10 GB verfügbar → Warnung geben +``` + +--- + +## Häufige Fragen + +**F: Kann ich install.sh mehrmals hintereinander ausführen?** +A: Ja. Script erkennt bestehende Installation (`.git` Verzeichnis) und führt `git pull` aus statt zu klonen. + +**F: Was passiert bei Netzwerk-Fehler während Download?** +A: Script stoppt mit Fehler (set -e). Nach Netzwerk-Reparatur kann man `bash install.sh` erneut ausführen. + +**F: Kann ich nur bestimmte Templates bauen?** +A: Ja. Ändere `.env`: `USER_TEMPLATE_IMAGES="user-template-01:latest"` (nur template-01) + +**F: Wie debugge ich Build-Fehler?** +A: Prüfe spawner-install.log: `cat spawner-install.log | grep -A 20 "ERROR\|FEHLER"` + +**F: Können User-Container nach Installation noch hinzugefügt werden?** +A: Ja. Neue `user-template-*` Ordner + .env update + `docker build` manuell oder nächster install.sh run. + +--- + +**Stand:** 2026-02-03 +**Version:** install.sh v0.1.0 +**Kompatibilität:** Bash 4.0+, BusyBox, Docker 20.10+, Docker Compose 2.0+