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:
2026-06-06 07:29:49 +02:00
parent 2f2e5b1f56
commit 69a0279bb7
7 changed files with 777 additions and 0 deletions

View 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 ==="

View 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

View 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"