feat(deploy): TrueNAS Custom-App + Auto-Deploy, plus aufgelaufene Arbeit
Deployment: - custom-app.compose.yaml: self-contained Compose fuer TrueNAS "Custom App" (absolute Host-Bind-Pfade, postgres:18, pull_policy always, Port 8090) - scripts/truenas-deploy.sh: Host-Skript create/redeploy via midclt (App bleibt unter Apps sichtbar) inkl. Image-Pull + Health-Check - ci.yml Deploy-Job: laeuft auf ubuntu-latest-Runner, kopiert Deploy-Dateien per SSH auf den NAS-Host und triggert truenas-deploy.sh (statt runs-on goldeye) - compose.yaml/.env.example: postgres:18 (Locale-Match zur Quell-DB), Port 8090 - .gitignore: .agents/, tools/rag/, deploy/truenas/.env (Secrets/Scratch) Aufgelaufene Feature-Arbeit (verified/Freeze, Migrationen, Import-Triage): - GerbilOverride/VerifiedGerbil-Endpoints + GerbilSnapshotService + Tests - EF-Migrationen (ShowInChronicle, Stillborn, BirthOrder, ManualFlag, DSGVO) - Frontend VerifizierteTierePage + verified-API + e2e-Spec - diverse Import-/Triage-Skripte und -Tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
# Sicheres Datenbankpasswort (mind. 20 Zeichen, keine Anführungszeichen)
|
||||
POSTGRES_PASSWORD=aendere_mich_bitte
|
||||
|
||||
# Externer Port fuer das Frontend (Standard: 80)
|
||||
PORT=80
|
||||
# Externer Port fuer das Frontend. NAS-Port 80 ist durch den nginx-Reverse-Proxy
|
||||
# belegt -> Standard 8090 (Zugriff: http://truenas:8090/).
|
||||
PORT=8090
|
||||
|
||||
# Container Registry (git.rismer.de/gulum)
|
||||
REGISTRY=git.rismer.de/gulum
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
services:
|
||||
|
||||
# --- PostgreSQL-Datenbank ---
|
||||
# postgres:18 (Debian): Locale en_US.utf8 == Quell-DB (Aspire) -> sauberer Dump-Restore.
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
image: postgres:18
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
|
||||
@@ -58,7 +59,7 @@ services:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -72,7 +73,8 @@ services:
|
||||
dockerfile: gerbil-manager-web/Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-80}:80"
|
||||
# NAS-Port 80 ist durch den nginx-Reverse-Proxy belegt -> Default 8090.
|
||||
- "${PORT:-8090}:80"
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
@@ -94,7 +96,7 @@ services:
|
||||
|
||||
# --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) ---
|
||||
backup:
|
||||
image: postgres:17-alpine
|
||||
image: postgres:18
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PGPASSWORD: "${POSTGRES_PASSWORD}"
|
||||
|
||||
97
deploy/truenas/custom-app.compose.yaml
Normal file
97
deploy/truenas/custom-app.compose.yaml
Normal file
@@ -0,0 +1,97 @@
|
||||
# GerbilManager — TrueNAS SCALE Custom App (Goldeye 25.10)
|
||||
# =========================================================
|
||||
# Diese Datei ist die VORLAGE fuer die TrueNAS-"Custom App" (taucht unter Apps auf).
|
||||
# Sie wird NICHT direkt mit `docker compose` gestartet, sondern von
|
||||
# deploy/truenas/scripts/truenas-deploy.sh
|
||||
# gerendert (Platzhalter aus deploy/truenas/.env eingesetzt) und via
|
||||
# midclt call app.create {custom_app:true, app_name:gerbilmanager, custom_compose_config_string:<yaml>}
|
||||
# an die TrueNAS-Middleware uebergeben. Redeploys laufen ueber `midclt call app.redeploy`.
|
||||
#
|
||||
# Unterschiede zur lokalen deploy/truenas/compose.yaml:
|
||||
# * ABSOLUTE Host-Bind-Pfade (Custom Apps kennen keine relativen ./-Mounts) —
|
||||
# gleiche Konvention wie die anderen Apps dieser NAS (/mnt/JailStorage/DockerVolumes/...).
|
||||
# * pull_policy: always -> `app.redeploy` zieht das frische :latest aus der Registry.
|
||||
# * kein publicsite-Container (oeffentliche Seite ist ein separater Schritt mit Domain/Proxy).
|
||||
#
|
||||
# Platzhalter (werden von truenas-deploy.sh aus .env ersetzt):
|
||||
# __POSTGRES_PASSWORD__ __REGISTRY__ __TAG__ __PORT__ __AI_BASEURL__ __AI_APIKEY__ __AI_MODEL__
|
||||
|
||||
services:
|
||||
|
||||
# --- PostgreSQL 18 (Debian-Image: Locale en_US.utf8 == Quell-DB) ---
|
||||
db:
|
||||
image: postgres:18
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "__POSTGRES_PASSWORD__"
|
||||
POSTGRES_DB: gerbilmanager
|
||||
POSTGRES_USER: postgres
|
||||
TZ: Europe/Berlin
|
||||
volumes:
|
||||
- /mnt/JailStorage/DockerVolumes/gerbilmanager/pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d gerbilmanager"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# --- .NET API ---
|
||||
api:
|
||||
image: "__REGISTRY__/gerbilmanager-api:__TAG__"
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ConnectionStrings__gerbilmanager: "Host=db;Port=5432;Database=gerbilmanager;Username=postgres;Password=__POSTGRES_PASSWORD__"
|
||||
Photos__RootPath: /data/photos
|
||||
DataProtection__KeyRingPath: /data/keys
|
||||
PublicSite__RootPath: /data/publicsite
|
||||
AI__BaseUrl: "__AI_BASEURL__"
|
||||
AI__ApiKey: "__AI_APIKEY__"
|
||||
AI__Model: "__AI_MODEL__"
|
||||
TZ: Europe/Berlin
|
||||
volumes:
|
||||
- /mnt/JailStorage/DockerVolumes/gerbilmanager/photos:/data/photos
|
||||
- /mnt/JailStorage/DockerVolumes/gerbilmanager/keys:/data/keys
|
||||
- /mnt/JailStorage/DockerVolumes/gerbilmanager/publicsite:/data/publicsite
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
|
||||
# --- Frontend (nginx: React-SPA + /api-Proxy) ---
|
||||
frontend:
|
||||
image: "__REGISTRY__/gerbilmanager-frontend:__TAG__"
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "__PORT__:80"
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
|
||||
# --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) ---
|
||||
backup:
|
||||
image: postgres:18
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PGPASSWORD: "__POSTGRES_PASSWORD__"
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: gerbilmanager
|
||||
BACKUP_KEEP_DAYS: "7"
|
||||
TZ: Europe/Berlin
|
||||
volumes:
|
||||
- /mnt/JailStorage/DockerVolumes/gerbilmanager/photos:/data/photos:ro
|
||||
- /mnt/JailStorage/DockerVolumes/gerbilmanager/backups:/backups
|
||||
- /mnt/JailStorage/DockerVolumes/gerbilmanager/scripts:/scripts:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
entrypoint: ["/bin/sh", "/scripts/entrypoint.sh"]
|
||||
@@ -1,6 +1,11 @@
|
||||
# GerbilManager — publicsite nginx (WEB-2)
|
||||
# Serviert die statische oeffentliche Seite aus dem live/-Verzeichnis des Shared Volumes.
|
||||
# SICHERHEIT: Kein Proxy auf die API, kein Zugriff auf den Manager.
|
||||
#
|
||||
# Security-Header (Art. 32 DSGVO — technische Massnahmen): MUESSEN pro location wiederholt
|
||||
# werden, da nginx server-weite add_header verwirft, sobald eine location eigene add_header hat.
|
||||
# HSTS wird NICHT hier gesetzt (HTTP-only-Container) — sondern am TLS-terminierenden
|
||||
# Reverse-Proxy (siehe deploy/truenas/vhost-snippet.conf).
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html/live;
|
||||
@@ -11,15 +16,23 @@ server {
|
||||
location / {
|
||||
try_files $uri $uri/index.html =404;
|
||||
add_header Cache-Control "no-cache, must-revalidate";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; form-action 'self'" always;
|
||||
}
|
||||
|
||||
# CSS/Bilder: kurze TTL (1 Tag)
|
||||
# CSS/Bilder: kurze TTL (1 Tag) — Security-Header wiederholen (s. o.)
|
||||
location ~* \.(css|png|jpg|jpeg|gif|ico|webp|svg)$ {
|
||||
try_files $uri =404;
|
||||
expires 1d;
|
||||
add_header Cache-Control "public, max-age=86400";
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; form-action 'self'" always;
|
||||
}
|
||||
|
||||
# Kein Zugriff auf Staging-Verzeichnisse
|
||||
|
||||
91
deploy/truenas/scripts/deploy.sh
Normal file
91
deploy/truenas/scripts/deploy.sh
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/bin/sh
|
||||
# GerbilManager Deploy-Skript (laeuft auf der TrueNAS Goldeye)
|
||||
# ===========================================================
|
||||
# Holt die neuen Images aus der Gitea-Registry und rollt den Compose-Stack neu aus.
|
||||
# Wird vom Gitea-Actions 'deploy'-Job (self-hosted Runner auf der NAS) aufgerufen
|
||||
# UND kann jederzeit manuell auf der NAS ausgefuehrt werden:
|
||||
#
|
||||
# sh /opt/gerbilmanager/deploy/truenas/scripts/deploy.sh
|
||||
# TAG=<git-sha> sh .../deploy.sh # Rollback auf ein bestimmtes Image
|
||||
# GIT_PULL=0 sh .../deploy.sh # ohne 'git pull' (nur Images neu ziehen)
|
||||
#
|
||||
# Konfiguration ueber Umgebungsvariablen (alle optional, sinnvolle Defaults):
|
||||
# COMPOSE_DIR Installationsverzeichnis (Default: /opt/gerbilmanager)
|
||||
# TAG Image-Tag (Default: latest; fuer Rollback z. B. ein git-SHA)
|
||||
# GIT_PULL 1 = vorher 'git pull' (Default), 0 = ueberspringen
|
||||
# HEALTH_TIMEOUT Sekunden auf api-Healthcheck warten (Default: 180)
|
||||
# REGISTRY / REGISTRY_USER / REGISTRY_TOKEN optionaler docker login vor dem Pull
|
||||
set -eu
|
||||
|
||||
COMPOSE_DIR="${COMPOSE_DIR:-/opt/gerbilmanager}"
|
||||
COMPOSE_FILE="$COMPOSE_DIR/deploy/truenas/compose.yaml"
|
||||
TAG="${TAG:-latest}"
|
||||
GIT_PULL="${GIT_PULL:-1}"
|
||||
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}"
|
||||
REGISTRY="${REGISTRY:-git.rismer.de}"
|
||||
|
||||
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; }
|
||||
fail() { log "FEHLER: $1"; exit 1; }
|
||||
|
||||
export TAG # compose interpoliert ${TAG} in den Image-Referenzen
|
||||
|
||||
log "=== Deploy gestartet (TAG=$TAG, dir=$COMPOSE_DIR) ==="
|
||||
|
||||
[ -f "$COMPOSE_FILE" ] || fail "compose.yaml nicht gefunden: $COMPOSE_FILE"
|
||||
command -v docker >/dev/null 2>&1 || fail "docker nicht im PATH des Runners"
|
||||
docker compose version >/dev/null 2>&1 || fail "'docker compose' (v2) nicht verfuegbar"
|
||||
|
||||
cd "$COMPOSE_DIR"
|
||||
|
||||
# --- 1. Repo aktualisieren (compose.yaml / Skripte / Docs) ---
|
||||
if [ "$GIT_PULL" = "1" ] && [ -d "$COMPOSE_DIR/.git" ]; then
|
||||
log "git pull --ff-only ..."
|
||||
git -C "$COMPOSE_DIR" pull --ff-only || log "WARNUNG: git pull fehlgeschlagen — fahre mit vorhandenem Stand fort"
|
||||
fi
|
||||
|
||||
# --- 2. Registry-Login (nur wenn Credentials uebergeben wurden) ---
|
||||
if [ -n "${REGISTRY_USER:-}" ] && [ -n "${REGISTRY_TOKEN:-}" ]; then
|
||||
log "docker login $REGISTRY ..."
|
||||
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin >/dev/null \
|
||||
|| fail "docker login fehlgeschlagen"
|
||||
fi
|
||||
|
||||
# --- 3. Neue Images ziehen ---
|
||||
log "Ziehe Images (Tag: $TAG) ..."
|
||||
docker compose -f "$COMPOSE_FILE" pull || fail "docker compose pull fehlgeschlagen"
|
||||
|
||||
# --- 4. Stack neu ausrollen (nur geaenderte Container werden neu erstellt) ---
|
||||
log "Rolle Stack aus (up -d) ..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d --remove-orphans || fail "docker compose up fehlgeschlagen"
|
||||
|
||||
# --- 5. Auf api-Healthcheck warten (impliziert db gesund) ---
|
||||
log "Warte auf api-Healthcheck (max ${HEALTH_TIMEOUT}s) ..."
|
||||
API_CID="$(docker compose -f "$COMPOSE_FILE" ps -q api)"
|
||||
[ -n "$API_CID" ] || fail "api-Container nicht gefunden"
|
||||
|
||||
elapsed=0
|
||||
while :; do
|
||||
state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$API_CID" 2>/dev/null || echo unknown)"
|
||||
case "$state" in
|
||||
healthy) log "api ist gesund."; break ;;
|
||||
none|unknown)
|
||||
# Kein Healthcheck definiert/lesbar -> auf laufenden Zustand zurueckfallen.
|
||||
running="$(docker inspect --format '{{.State.Running}}' "$API_CID" 2>/dev/null || echo false)"
|
||||
[ "$running" = "true" ] && { log "api laeuft (kein Healthcheck-Status lesbar)."; break; }
|
||||
;;
|
||||
esac
|
||||
if [ "$elapsed" -ge "$HEALTH_TIMEOUT" ]; then
|
||||
log "Letzte api-Logs:"; docker compose -f "$COMPOSE_FILE" logs --tail 40 api || true
|
||||
fail "api wurde nicht innerhalb ${HEALTH_TIMEOUT}s gesund (Status: $state)"
|
||||
fi
|
||||
sleep 5; elapsed=$((elapsed + 5))
|
||||
done
|
||||
|
||||
# --- 6. Alte/ungenutzte Images aufraeumen ---
|
||||
log "Raeume verwaiste Images auf ..."
|
||||
docker image prune -f >/dev/null 2>&1 || true
|
||||
|
||||
log "Laufende Dienste:"
|
||||
docker compose -f "$COMPOSE_FILE" ps --format 'table {{.Service}}\t{{.Status}}' 2>/dev/null \
|
||||
|| docker compose -f "$COMPOSE_FILE" ps
|
||||
log "=== Deploy erfolgreich abgeschlossen (TAG=$TAG) ==="
|
||||
103
deploy/truenas/scripts/truenas-deploy.sh
Normal file
103
deploy/truenas/scripts/truenas-deploy.sh
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/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
|
||||
COMPOSE_DIR="${COMPOSE_DIR:-/opt/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}"
|
||||
|
||||
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 + midclt-Payload bauen (python: sichere Escapes) ---
|
||||
PAYLOAD_FILE="$(mktemp)"
|
||||
trap 'rm -f "$PAYLOAD_FILE"' EXIT
|
||||
python3 - "$TEMPLATE" > "$PAYLOAD_FILE" <<'PY'
|
||||
import sys, os, json
|
||||
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"),
|
||||
"__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)
|
||||
print(json.dumps({"custom_app": True, "app_name": "gerbilmanager",
|
||||
"custom_compose_config_string": tmpl}))
|
||||
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 -> redeploy (frische Images sind bereits gezogen)"
|
||||
midclt call app.redeploy "$APP_NAME" >/dev/null || fail "app.redeploy fehlgeschlagen"
|
||||
else
|
||||
log "App fehlt -> app.create (Custom App)"
|
||||
midclt call app.create "$(cat "$PAYLOAD_FILE")" >/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) ==="
|
||||
@@ -1,37 +1,63 @@
|
||||
# GerbilManager — Externer nginx-Vhost fuer die oeffentliche Webseite (WEB-2)
|
||||
# In Julians bestehenden nginx-Reverse-Proxy einfuegen.
|
||||
# <DOMAIN> ersetzen sobald der Hostname feststeht (Julian liefert ihn).
|
||||
# GerbilManager — Vhost fuer die oeffentliche Webseite (WEB-2)
|
||||
# ===========================================================
|
||||
# Einzufuegen in den bestehenden nginx-Reverse-Proxy (TrueNAS-App "ix-webserver"):
|
||||
# Datei ablegen unter: /mnt/JailStorage/DockerVolumes/nginx/config/gerbil-public.conf
|
||||
# (das ist der conf.d-Mount des Proxys; dort liegen auch git/cloud/rismer.de)
|
||||
# Danach: docker exec ix-webserver-webserver-1 nginx -s reload
|
||||
#
|
||||
# SICHERHEIT: Dieser Vhost zeigt NUR auf den publicsite-Container (Port 8081).
|
||||
# Der Manager (API + Frontend, Port 80) ist NICHT erreichbar von aussen —
|
||||
# er hat keine Authentifizierung und muss LAN-only bleiben.
|
||||
# WICHTIG (containerisierter Proxy): das Backend MUSS ueber die Host-LAN-IP
|
||||
# angesprochen werden (192.168.2.115), NICHT 127.0.0.1 — 127.0.0.1 waere im
|
||||
# Proxy-Container er selbst. Der publicsite-Container veroeffentlicht Port 8081
|
||||
# auf dem Host (PUBLICSITE_PORT in deploy/truenas/.env).
|
||||
#
|
||||
# SICHERHEIT: Dieser Vhost zeigt NUR auf den publicsite-Container (statische
|
||||
# oeffentliche Seite). Der Manager (API + Frontend) bleibt LAN-only und ist von
|
||||
# aussen NICHT erreichbar (keine Authentifizierung).
|
||||
#
|
||||
# <DOMAIN> ersetzen (z. B. rennmaus.rismer.de). DNS-Eintrag auf die oeffentliche
|
||||
# IP + Router-Weiterleitung 80/443 -> 11080/11443 muss wie bei den anderen
|
||||
# rismer.de-Subdomains vorhanden sein.
|
||||
|
||||
# HTTP: ACME-Challenge zulassen + auf HTTPS umleiten (Muster der anderen Vhosts)
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name <DOMAIN>;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/letsencrypt;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS: Reverse-Proxy auf den publicsite-Container (Host-Port 8081)
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name <DOMAIN>;
|
||||
|
||||
# Let's Encrypt (gleicher Cert-Store wie die uebrigen rismer.de-Vhosts).
|
||||
# Zertifikat anfordern, z. B.:
|
||||
# docker exec ix-webserver-webserver-1 certbot --nginx -d <DOMAIN>
|
||||
# oder webroot: certbot certonly --webroot -w /usr/share/nginx/letsencrypt -d <DOMAIN>
|
||||
ssl_certificate /etc/letsencrypt/live/<DOMAIN>/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/<DOMAIN>/privkey.pem;
|
||||
|
||||
# HSTS gehoert auf die TLS-terminierende Ebene (hier), nicht in den HTTP-only
|
||||
# publicsite-Container. Erst aktivieren, wenn HTTPS sicher steht (max-age dann ggf. erhoehen).
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
# Die uebrigen Security-Header (CSP, X-Content-Type-Options, X-Frame-Options,
|
||||
# Referrer-Policy, Permissions-Policy) liefert der publicsite-nginx bereits mit und
|
||||
# werden vom Proxy durchgereicht (siehe nginx/publicsite.conf).
|
||||
|
||||
location / {
|
||||
proxy_pass http://192.168.2.115:8081;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Kein Buffering fuer kleine statische HTML-Seiten
|
||||
proxy_buffering off;
|
||||
proxy_buffering off; # kleine statische HTML-Seiten
|
||||
}
|
||||
}
|
||||
|
||||
# Fuer HTTPS (empfohlen, z.B. per Let's Encrypt via certbot):
|
||||
# server {
|
||||
# listen 443 ssl;
|
||||
# server_name <DOMAIN>;
|
||||
# ssl_certificate /etc/letsencrypt/live/<DOMAIN>/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/<DOMAIN>/privkey.pem;
|
||||
# location / {
|
||||
# proxy_pass http://127.0.0.1:8081;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
# }
|
||||
|
||||
Reference in New Issue
Block a user