spawner/docs/install/INSTALL_SCRIPT_ANALYSIS.md
2026-02-03 14:11:58 +01:00

34 KiB

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

#!/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

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

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

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

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

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

echo "============================================================"
echo "  Container Spawner Installation v${VERSION}"
echo "============================================================"
  • Zeige Script-Version an
  • Initialisiere Log-Datei mit Timestamp

Zeilen 87-100: .env Existenz-Check

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

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

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:

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

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

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)

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)

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)

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

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

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

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

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

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

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 .envTRAEFIK_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

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

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

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):

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):

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

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:

# 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

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)

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)

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):

# 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):

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):

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):

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:

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:

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

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

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

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

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:

Zeilen 670-678: Lokale URLs & Befehle

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

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:

# 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

→ Installiere Docker ≥ 20.10
→ Starte install.sh erneut

Fehler: .env nicht gefunden

→ cp .env.example .env
→ nano .env  (setze SECRET_KEY, BASE_DOMAIN etc.)
→ bash install.sh

Fehler: Template-Image Build fehlt

→ 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

→ 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

# 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

# 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+