#!/bin/sh # GerbilManager — TrueNAS Custom-App Deploy/Redeploy (laeuft auf dem NAS-HOST) # =========================================================================== # Legt die App als TrueNAS "Custom App" an (sichtbar unter Apps) bzw. rollt sie # neu aus. Wird vom Gitea-Actions-Deploy-Job per SSH aufgerufen UND kann jederzeit # manuell auf dem NAS-Host ausgefuehrt werden: # # sh /opt/gerbilmanager/deploy/truenas/scripts/truenas-deploy.sh # # Voraussetzungen auf dem Host: # * /opt/gerbilmanager/deploy/truenas/custom-app.compose.yaml (Vorlage) # * /opt/gerbilmanager/deploy/truenas/.env (mit POSTGRES_PASSWORD etc. — NICHT im Repo) # * midclt + docker im PATH (auf TrueNAS SCALE gegeben) # # Env-Overrides: COMPOSE_DIR, ENV_FILE, HEALTH_TIMEOUT set -eu APP_NAME=gerbilmanager # TrueNAS Goldeye: / (inkl. /opt) ist read-only -> App-Home liegt auf dem Pool. COMPOSE_DIR="${COMPOSE_DIR:-/mnt/JailStorage/DockerVolumes/gerbilmanager}" DEPLOY_DIR="$COMPOSE_DIR/deploy/truenas" ENV_FILE="${ENV_FILE:-$DEPLOY_DIR/.env}" TEMPLATE="$DEPLOY_DIR/custom-app.compose.yaml" HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-300}" BASE=/mnt/JailStorage/DockerVolumes/gerbilmanager log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; } fail() { log "FEHLER: $*"; exit 1; } [ -f "$TEMPLATE" ] || fail "Vorlage fehlt: $TEMPLATE" [ -f "$ENV_FILE" ] || fail ".env fehlt: $ENV_FILE (aus .env.example erstellen)" command -v midclt >/dev/null 2>&1 || fail "midclt nicht im PATH" command -v docker >/dev/null 2>&1 || fail "docker nicht im PATH" # --- .env laden --- set -a; . "$ENV_FILE"; set +a : "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env}" export REGISTRY="${REGISTRY:-git.rismer.de/gulum}" export TAG="${TAG:-latest}" export PORT="${PORT:-8090}" export PUBLICSITE_PORT="${PUBLICSITE_PORT:-8081}" log "=== Deploy $APP_NAME (TAG=$TAG, PORT=$PORT) ===" # --- Host-Verzeichnisse sicherstellen --- for d in pgdata photos keys backups publicsite scripts; do mkdir -p "$BASE/$d"; done chown -R 999:999 "$BASE/pgdata" 2>/dev/null || true chmod 700 "$BASE/pgdata" 2>/dev/null || true # Backup-Sidecar-Skripte an den absoluten Mount-Pfad kopieren cp "$DEPLOY_DIR/scripts/backup.sh" "$DEPLOY_DIR/scripts/entrypoint.sh" \ "$DEPLOY_DIR/scripts/restore.sh" "$BASE/scripts/" 2>/dev/null || true # --- Frische Images ziehen (garantiert :latest, unabhaengig vom pull_policy) --- log "Ziehe Images ..." docker pull "$REGISTRY/gerbilmanager-api:$TAG" || fail "docker pull api fehlgeschlagen" docker pull "$REGISTRY/gerbilmanager-frontend:$TAG" || fail "docker pull frontend fehlgeschlagen" # --- Vorlage rendern (python: sichere Substitution) --- RENDERED="$(mktemp)" trap 'rm -f "$RENDERED"' EXIT python3 - "$TEMPLATE" > "$RENDERED" <<'PY' import sys, os tmpl = open(sys.argv[1], encoding="utf-8").read() repl = { "__POSTGRES_PASSWORD__": os.environ["POSTGRES_PASSWORD"], "__REGISTRY__": os.environ.get("REGISTRY", "git.rismer.de/gulum"), "__TAG__": os.environ.get("TAG", "latest"), "__PORT__": os.environ.get("PORT", "8090"), "__PUBLICSITE_PORT__": os.environ.get("PUBLICSITE_PORT", "8081"), "__AI_BASEURL__": os.environ.get("AI__BaseUrl", ""), "__AI_APIKEY__": os.environ.get("AI__ApiKey", ""), "__AI_MODEL__": os.environ.get("AI__Model", "gemini-flash-latest"), } for k, v in repl.items(): tmpl = tmpl.replace(k, v) sys.stdout.write(tmpl) PY # midclt-Payload bauen: create braucht app_name+custom_app, update nur die Compose. build_payload() { # $1 = create|update python3 - "$RENDERED" "$1" <<'PY' import sys, json compose = open(sys.argv[1], encoding="utf-8").read() if sys.argv[2] == "create": print(json.dumps({"custom_app": True, "app_name": "gerbilmanager", "custom_compose_config_string": compose})) else: print(json.dumps({"custom_compose_config_string": compose})) PY } # --- Existiert die App schon? --- EXISTS="$(midclt call app.query "[[\"name\",\"=\",\"$APP_NAME\"]]" 2>/dev/null | python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null || echo 0)" if [ "$EXISTS" -ge 1 ]; then log "App existiert -> app.update (Compose-Aenderungen uebernehmen, frische Images gezogen)" midclt call app.update "$APP_NAME" "$(build_payload update)" >/dev/null || fail "app.update fehlgeschlagen" else log "App fehlt -> app.create (Custom App)" midclt call app.create "$(build_payload create)" >/dev/null || fail "app.create fehlgeschlagen" fi # --- Auf Gesundheit warten (Frontend proxyt /api/health -> API) --- log "Warte auf /api/health (max ${HEALTH_TIMEOUT}s) ..." elapsed=0 while :; do if curl -fsS "http://localhost:$PORT/api/health" 2>/dev/null | grep -qi 'healthy'; then log "App ist gesund."; break fi [ "$elapsed" -ge "$HEALTH_TIMEOUT" ] && { \ log "Letzte App-Logs:"; midclt call app.query "[[\"name\",\"=\",\"$APP_NAME\"]]" >/dev/null 2>&1 || true; \ fail "App wurde nicht innerhalb ${HEALTH_TIMEOUT}s gesund"; } sleep 5; elapsed=$((elapsed + 5)) done log "Aufraeumen ungenutzter Images ..." docker image prune -f >/dev/null 2>&1 || true log "=== Deploy erfolgreich ($APP_NAME, TAG=$TAG) ==="