OPS-1: TrueNAS compose, backup sidecar, Gitea CI draft, ops docs
deploy/truenas/compose.yaml:
- 4 services: db (postgres:17), api (.NET), frontend (nginx), backup sidecar
- Named volumes as NAS bind-mounts (paths configured in .env)
- Healthchecks: db pg_isready, api /health, frontend waits on api
- ANTHROPIC_API_KEY placeholder for FEAT-12a AI stub
deploy/truenas/.env.example: POSTGRES_PASSWORD, PORT, REGISTRY, dataset paths
deploy/truenas/scripts/:
- entrypoint.sh: installs daily 03:00 cron, starts crond in foreground
- backup.sh: pg_dump -> .sql + tar czf photos -> .tar.gz + rotation
- restore.sh: psql < dump + tar xzf photos; confirmation prompt (-f to skip)
Predecessor logic (commit 6a37f9e) translated from PowerShell to POSIX sh
for Alpine containers. Same pg_dump/restore semantics, same KEEP_DAYS rotation.
.gitea/workflows/ci.yml (DRAFT -- inactive until Julian confirms Actions):
dotnet test + npm test/build on push/PR to main;
docker build+push to Gitea registry on main push only
docs/ops.md:
German ops guide -- architecture diagram, first-install checklist,
start/stop/update commands, backup runbook (manual + auto), ZFS-snapshot
layering strategy, CI activation steps, open questions, troubleshooting table
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
23
deploy/truenas/.env.example
Normal file
23
deploy/truenas/.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# GerbilManager Produktionskonfiguration
|
||||
# Kopiere diese Datei nach .env und setze die Werte vor dem ersten Start.
|
||||
|
||||
# Sicheres Datenbankpasswort (mind. 20 Zeichen, keine Anführungszeichen)
|
||||
POSTGRES_PASSWORD=aendere_mich_bitte
|
||||
|
||||
# Externer Port fuer das Frontend (Standard: 80)
|
||||
PORT=80
|
||||
|
||||
# Gitea Container Registry (Standard: truenas:13000/gulum)
|
||||
REGISTRY=truenas:13000/gulum
|
||||
TAG=latest
|
||||
|
||||
# NAS-Dataset-Pfade (TrueNAS SCALE: /mnt/<Pool>/<Dataset>)
|
||||
PGDATA_PATH=/mnt/SSD/gerbil/pgdata
|
||||
PHOTOS_PATH=/mnt/SSD/gerbil/photos
|
||||
BACKUPS_PATH=/mnt/SSD/gerbil/backups
|
||||
|
||||
# Backup-Rotation: Anzahl Tage (Standard: 7)
|
||||
BACKUP_KEEP_DAYS=7
|
||||
|
||||
# Claude-API-Key fuer KI-Verkaufstext (FEAT-12a; leer lassen wenn nicht vorhanden)
|
||||
ANTHROPIC_API_KEY=
|
||||
109
deploy/truenas/compose.yaml
Normal file
109
deploy/truenas/compose.yaml
Normal file
@@ -0,0 +1,109 @@
|
||||
# GerbilManager — TrueNAS Custom Application
|
||||
# ==============================================
|
||||
# Vor dem ersten Start:
|
||||
# 1. Kopiere deploy/truenas/.env.example -> deploy/truenas/.env und setze die Werte.
|
||||
# 2. Lege die Dataset-Pfade auf der NAS an (pgdata, photos, backups).
|
||||
# 3. docker compose -f deploy/truenas/compose.yaml up -d
|
||||
#
|
||||
# Zugriff: http://<NAS-IP>:${PORT:-80}
|
||||
# API-Doku: http://<NAS-IP>:${PORT:-80}/scalar
|
||||
|
||||
services:
|
||||
|
||||
# --- PostgreSQL-Datenbank ---
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
|
||||
POSTGRES_DB: gerbilmanager
|
||||
POSTGRES_USER: postgres
|
||||
volumes:
|
||||
- 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 (GerbilManagerWebAPI) ---
|
||||
api:
|
||||
image: "${REGISTRY:-truenas:13000/gulum}/gerbilmanager-api:${TAG:-latest}"
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: GerbilManagerWebAPI/Dockerfile
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
# Verbindung zur Postgres-DB im selben Compose-Netz
|
||||
ConnectionStrings__gerbilmanager: "Host=db;Port=5432;Database=gerbilmanager;Username=postgres;Password=${POSTGRES_PASSWORD}"
|
||||
# Speicherort der hochgeladenen Fotos (NAS-Dataset gemounted unter /data/photos)
|
||||
Photos__RootPath: /data/photos
|
||||
# KI-Verkaufstext (FEAT-12a stub; leer lassen wenn kein Key vorhanden)
|
||||
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
|
||||
volumes:
|
||||
- photos:/data/photos
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
|
||||
# --- nginx Frontend (React SPA + API-Proxy) ---
|
||||
frontend:
|
||||
image: "${REGISTRY:-truenas:13000/gulum}/gerbilmanager-frontend:${TAG:-latest}"
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: gerbil-manager-web/Dockerfile
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-80}:80"
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
|
||||
# --- Backup-Sidecar (taeglicher pg_dump + Foto-Archiv + Rotation) ---
|
||||
backup:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PGPASSWORD: "${POSTGRES_PASSWORD}"
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: gerbilmanager
|
||||
BACKUP_KEEP_DAYS: "${BACKUP_KEEP_DAYS:-7}"
|
||||
volumes:
|
||||
- photos:/data/photos:ro
|
||||
- backups:/backups
|
||||
- ./scripts:/scripts:ro
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
entrypoint: ["/bin/sh", "/scripts/entrypoint.sh"]
|
||||
|
||||
volumes:
|
||||
# NAS-Datasets als Bind-Mounts (Pfade in .env konfigurieren).
|
||||
# TrueNAS: Dataset-Pfad z.B. /mnt/SSD/gerbil/pgdata
|
||||
pgdata:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: "${PGDATA_PATH:-/mnt/gerbil/pgdata}"
|
||||
photos:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: "${PHOTOS_PATH:-/mnt/gerbil/photos}"
|
||||
backups:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: "${BACKUPS_PATH:-/mnt/gerbil/backups}"
|
||||
56
deploy/truenas/scripts/backup.sh
Normal file
56
deploy/truenas/scripts/backup.sh
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
# GerbilManager Backup-Skript (Container-Welt)
|
||||
# Laeuft als Cron-Job im backup-Sidecar-Container.
|
||||
# Volumes: /data/photos (read-only), /backups (read-write)
|
||||
# Umgebungsvariablen: PGPASSWORD, POSTGRES_HOST, POSTGRES_USER, POSTGRES_DB, BACKUP_KEEP_DAYS
|
||||
set -e
|
||||
|
||||
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
|
||||
BACKUP_DIR="/backups/$TIMESTAMP"
|
||||
DB_DUMP="$BACKUP_DIR/gerbilmanager_${TIMESTAMP}.sql"
|
||||
PHOTO_ARCHIVE="$BACKUP_DIR/photos_${TIMESTAMP}.tar.gz"
|
||||
KEEP_DAYS="${BACKUP_KEEP_DAYS:-7}"
|
||||
HOST="${POSTGRES_HOST:-db}"
|
||||
USER="${POSTGRES_USER:-postgres}"
|
||||
DB="${POSTGRES_DB:-gerbilmanager}"
|
||||
|
||||
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"; }
|
||||
|
||||
log "=== Backup gestartet ($TIMESTAMP) ==="
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# --- Datenbank-Dump ---
|
||||
log "Erstelle pg_dump fuer $DB@$HOST..."
|
||||
if ! pg_dump -h "$HOST" -U "$USER" -d "$DB" --clean --if-exists --format=plain \
|
||||
> "$DB_DUMP" 2>/tmp/pg_err; then
|
||||
log "FEHLER bei pg_dump: $(cat /tmp/pg_err)"
|
||||
exit 1
|
||||
fi
|
||||
DUMP_SIZE=$(du -k "$DB_DUMP" | cut -f1)
|
||||
log "Dump erstellt: $DB_DUMP (${DUMP_SIZE} KB)"
|
||||
|
||||
# Validierung: mindestens CREATE TABLE muss vorhanden sein
|
||||
if ! grep -q "CREATE TABLE" "$DB_DUMP" 2>/dev/null; then
|
||||
log "WARNUNG: Dump sieht ungueltig aus -- bitte $DB_DUMP pruefen"
|
||||
fi
|
||||
|
||||
# --- Fotos archivieren ---
|
||||
if [ -d /data/photos ] && [ "$(ls -A /data/photos 2>/dev/null)" ]; then
|
||||
PHOTO_COUNT=$(find /data/photos -type f | wc -l)
|
||||
log "Archiviere $PHOTO_COUNT Fotos..."
|
||||
tar czf "$PHOTO_ARCHIVE" -C /data photos/ 2>/tmp/tar_err || {
|
||||
log "WARNUNG: Foto-Archiv fehlgeschlagen: $(cat /tmp/tar_err)"
|
||||
}
|
||||
PHOTO_SIZE=$(du -k "$PHOTO_ARCHIVE" 2>/dev/null | cut -f1)
|
||||
log "Foto-Archiv erstellt: $PHOTO_ARCHIVE (${PHOTO_SIZE} KB)"
|
||||
else
|
||||
log "Keine Fotos gefunden -- Foto-Backup uebersprungen"
|
||||
fi
|
||||
|
||||
# --- Rotation: alte Backups loeschen ---
|
||||
log "Rotiere Backups (behalte $KEEP_DAYS Tage)..."
|
||||
find /backups -maxdepth 1 -type d -name '????-??-??_??-??' \
|
||||
-mtime "+$KEEP_DAYS" -exec rm -rf {} + 2>/dev/null || true
|
||||
REMAINING=$(find /backups -maxdepth 1 -type d -name '????-??-??_??-??' | wc -l)
|
||||
log "Backup abgeschlossen. Vorhandene Sicherungen: $REMAINING"
|
||||
log "=== Ende ==="
|
||||
16
deploy/truenas/scripts/entrypoint.sh
Normal file
16
deploy/truenas/scripts/entrypoint.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Backup-Sidecar Entrypoint
|
||||
# Installiert den Cron-Job und startet crond im Vordergrund.
|
||||
set -e
|
||||
|
||||
# Cron-Job: taeglich um 03:00 Uhr
|
||||
CRON_SCHEDULE="${BACKUP_CRON:-0 3 * * *}"
|
||||
echo "$CRON_SCHEDULE /bin/sh /scripts/backup.sh >> /backups/backup.log 2>&1" > /etc/crontabs/root
|
||||
|
||||
echo "[$(date)] Backup-Sidecar gestartet. Naechste Sicherung: $CRON_SCHEDULE"
|
||||
echo "[$(date)] Backup-Verzeichnis: /backups Fotos: /data/photos"
|
||||
|
||||
# Ersten Backup-Lauf direkt beim Start ausfuehren (optional, auskommentieren wenn unerwuenscht)
|
||||
# /bin/sh /scripts/backup.sh
|
||||
|
||||
exec crond -f -l 6
|
||||
77
deploy/truenas/scripts/restore.sh
Normal file
77
deploy/truenas/scripts/restore.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh
|
||||
# GerbilManager Wiederherstellung (Container-Welt)
|
||||
# Aufruf: docker compose -f deploy/truenas/compose.yaml exec backup /bin/sh /scripts/restore.sh
|
||||
# Optional: .../restore.sh 2026-06-06_03-00 (bestimmtes Backup)
|
||||
#
|
||||
# WARNUNG: Ueberschreibt alle aktuellen Datenbankdaten und Fotos!
|
||||
set -e
|
||||
|
||||
HOST="${POSTGRES_HOST:-db}"
|
||||
USER="${POSTGRES_USER:-postgres}"
|
||||
DB="${POSTGRES_DB:-gerbilmanager}"
|
||||
BACKUP_ROOT=/backups
|
||||
|
||||
log() { echo "[$(date +'%H:%M:%S')] $1"; }
|
||||
|
||||
# --- Backup-Verzeichnis bestimmen ---
|
||||
if [ -n "$1" ]; then
|
||||
BACKUP_DIR="$BACKUP_ROOT/$1"
|
||||
else
|
||||
BACKUP_DIR=$(find "$BACKUP_ROOT" -maxdepth 1 -type d -name '????-??-??_??-??' \
|
||||
| sort -r | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$BACKUP_DIR" ] || [ ! -d "$BACKUP_DIR" ]; then
|
||||
log "FEHLER: Backup-Verzeichnis nicht gefunden: ${BACKUP_DIR:-$BACKUP_ROOT}"
|
||||
log "Verfuegbare Backups:"
|
||||
find "$BACKUP_ROOT" -maxdepth 1 -type d -name '????-??-??_??-??' | sort -r
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SQL_FILE=$(find "$BACKUP_DIR" -name '*.sql' | head -1)
|
||||
PHOTO_ARCHIVE=$(find "$BACKUP_DIR" -name '*.tar.gz' | head -1)
|
||||
|
||||
if [ -z "$SQL_FILE" ]; then
|
||||
log "FEHLER: Kein SQL-Dump in $BACKUP_DIR gefunden."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Wiederherstellung aus: $BACKUP_DIR"
|
||||
log " Datenbank : $(basename $SQL_FILE)"
|
||||
[ -n "$PHOTO_ARCHIVE" ] && log " Fotos : $(basename $PHOTO_ARCHIVE)"
|
||||
|
||||
# --- Bestaetigung (interaktiv; mit -f ueberspringen) ---
|
||||
if [ "$1" != "-f" ] && [ "$2" != "-f" ]; then
|
||||
printf "WARNUNG: Aktuelle Daten werden unwiderruflich ueberschrieben!\nFortfahren? (ja/nein): "
|
||||
read -r CONFIRM
|
||||
if [ "$CONFIRM" != "ja" ]; then
|
||||
log "Abgebrochen."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Datenbankwiederherstellung ---
|
||||
log "Trenne laufende DB-Verbindungen..."
|
||||
psql -h "$HOST" -U "$USER" -d postgres -c \
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$DB' AND pid<>pg_backend_pid();" \
|
||||
> /dev/null 2>&1 || true
|
||||
|
||||
log "Spiele SQL-Dump ein..."
|
||||
if ! psql -h "$HOST" -U "$USER" -d "$DB" < "$SQL_FILE" > /tmp/restore_out 2>&1; then
|
||||
log "FEHLER bei der DB-Wiederherstellung:"
|
||||
cat /tmp/restore_out
|
||||
exit 1
|
||||
fi
|
||||
log "Datenbankwiederherstellung erfolgreich."
|
||||
|
||||
# --- Fotos wiederherstellen ---
|
||||
if [ -n "$PHOTO_ARCHIVE" ]; then
|
||||
log "Stelle Fotos wieder her..."
|
||||
rm -rf /data/photos/* 2>/dev/null || true
|
||||
tar xzf "$PHOTO_ARCHIVE" -C /data
|
||||
PHOTO_COUNT=$(find /data/photos -type f 2>/dev/null | wc -l)
|
||||
log "Fotos wiederhergestellt: $PHOTO_COUNT Dateien."
|
||||
fi
|
||||
|
||||
log "Wiederherstellung abgeschlossen."
|
||||
log "Starte API-Container neu: docker compose ... restart api"
|
||||
Reference in New Issue
Block a user