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

1107 lines
34 KiB
Markdown

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