Ops: add Proxmox migration tooling and runbook
Add end-to-end migration scripts for inventory, precopy, cutover, smoke tests, rollback, and post-migration checks. Include an operational runbook and Proxmox env template to move Hördle behind Nginx Proxy Manager while preserving persistent volumes safely.
This commit is contained in:
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Erstellt ein lokales Backup der Persistenzdaten.
|
||||
# Ziel: data/prod.db + public/uploads
|
||||
|
||||
APP_DIR="${APP_DIR:-$(pwd)}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-$APP_DIR/backups/persistence}"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
OUT_DIR="$BACKUP_DIR/$TIMESTAMP"
|
||||
|
||||
cd "$APP_DIR"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
echo "== Hördle Persistenz-Backup =="
|
||||
echo "Quelle: $APP_DIR"
|
||||
echo "Ziel: $OUT_DIR"
|
||||
|
||||
if [ -f "data/prod.db" ]; then
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
sqlite3 "data/prod.db" ".timeout 5000" ".backup '$OUT_DIR/prod.db'"
|
||||
else
|
||||
cp "data/prod.db" "$OUT_DIR/prod.db"
|
||||
fi
|
||||
sha256sum "$OUT_DIR/prod.db" > "$OUT_DIR/prod.db.sha256"
|
||||
else
|
||||
echo "Warnung: data/prod.db fehlt."
|
||||
fi
|
||||
|
||||
if [ -d "public/uploads" ]; then
|
||||
tar -C "public" -czf "$OUT_DIR/uploads.tar.gz" "uploads"
|
||||
sha256sum "$OUT_DIR/uploads.tar.gz" > "$OUT_DIR/uploads.tar.gz.sha256"
|
||||
else
|
||||
echo "Warnung: public/uploads fehlt."
|
||||
fi
|
||||
|
||||
echo "$TIMESTAMP" > "$BACKUP_DIR/latest"
|
||||
echo "Backup abgeschlossen: $OUT_DIR"
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Cutover-Skript fuer kurzes Wartungsfenster.
|
||||
# Auf QUELLSYSTEM ausfuehren:
|
||||
# 1) stoppt lokale Hördle-App
|
||||
# 2) finaler Delta-Sync data/ + uploads/
|
||||
# 3) startet Ziel-App im Proxmox-LXC
|
||||
|
||||
TARGET_HOST="${TARGET_HOST:-root@10.0.0.19}"
|
||||
TARGET_APP_DIR="${TARGET_APP_DIR:-/opt/hoerdle}"
|
||||
SOURCE_APP_DIR="${SOURCE_APP_DIR:-$(pwd)}"
|
||||
SOURCE_COMPOSE_FILE="${SOURCE_COMPOSE_FILE:-docker-compose.yml}"
|
||||
TARGET_COMPOSE_FILE="${TARGET_COMPOSE_FILE:-docker-compose.yml}"
|
||||
SSH_OPTS="${SSH_OPTS:-}"
|
||||
|
||||
cd "$SOURCE_APP_DIR"
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Fehlend: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd docker
|
||||
require_cmd rsync
|
||||
require_cmd ssh
|
||||
require_cmd sha256sum
|
||||
|
||||
echo "== Hördle Cutover =="
|
||||
echo "Quelle: $(pwd)"
|
||||
echo "Ziel: $TARGET_HOST:$TARGET_APP_DIR"
|
||||
echo
|
||||
|
||||
echo "Wartungsfenster starten. Enter zum Fortfahren, Ctrl+C zum Abbruch."
|
||||
read -r
|
||||
|
||||
echo "-- Quelle stoppen --"
|
||||
docker compose -f "$SOURCE_COMPOSE_FILE" down
|
||||
echo
|
||||
|
||||
echo "-- Finaler Delta-Sync data/ --"
|
||||
rsync -aHAX --numeric-ids --delete \
|
||||
-e "ssh $SSH_OPTS" \
|
||||
"./data/" "$TARGET_HOST:$TARGET_APP_DIR/data/"
|
||||
echo
|
||||
|
||||
echo "-- Finaler Delta-Sync public/uploads/ --"
|
||||
rsync -aHAX --numeric-ids --delete \
|
||||
-e "ssh $SSH_OPTS" \
|
||||
"./public/uploads/" "$TARGET_HOST:$TARGET_APP_DIR/public/uploads/"
|
||||
echo
|
||||
|
||||
if [ -f "./data/prod.db" ]; then
|
||||
echo "-- Konsistenzcheck DB --"
|
||||
LOCAL_HASH="$(sha256sum ./data/prod.db | awk '{print $1}')"
|
||||
REMOTE_HASH="$(ssh $SSH_OPTS "$TARGET_HOST" "sha256sum '$TARGET_APP_DIR/data/prod.db' | awk '{print \$1}'")"
|
||||
echo "local prod.db sha256: $LOCAL_HASH"
|
||||
echo "remote prod.db sha256: $REMOTE_HASH"
|
||||
|
||||
if [ "$LOCAL_HASH" != "$REMOTE_HASH" ]; then
|
||||
echo "DB Hash mismatch. Abbruch vor Start auf Ziel."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "-- Ziel-App starten --"
|
||||
ssh $SSH_OPTS "$TARGET_HOST" "cd '$TARGET_APP_DIR' && docker compose -f '$TARGET_COMPOSE_FILE' up -d --build"
|
||||
echo
|
||||
|
||||
echo "Cutover abgeschlossen. Bitte jetzt scripts/migration-smoke-test.sh ausfuehren."
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Inventarisiert Persistenz und laufzeitrelevante Variablen für die Migration.
|
||||
# Das Skript verändert keine Daten.
|
||||
|
||||
ROOT_DIR="${1:-$(pwd)}"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
count_files_recursive() {
|
||||
local dir="$1"
|
||||
find "$dir" -type f 2>/dev/null | wc -l
|
||||
}
|
||||
|
||||
echo "== Hördle Migrations-Inventur =="
|
||||
echo "Projektpfad: $(pwd)"
|
||||
echo
|
||||
|
||||
required_paths=(
|
||||
"data"
|
||||
"data/prod.db"
|
||||
"public/uploads"
|
||||
"docker-compose.yml"
|
||||
".env"
|
||||
)
|
||||
|
||||
echo "-- Existenzcheck --"
|
||||
for path in "${required_paths[@]}"; do
|
||||
if [ -e "$path" ]; then
|
||||
echo "[OK] $path"
|
||||
else
|
||||
echo "[FEHLT] $path"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
|
||||
echo "-- Groesse und Dateizaehlung --"
|
||||
if [ -d "data" ]; then
|
||||
du -sh "data"
|
||||
DATA_FILE_COUNT="$(count_files_recursive "data")"
|
||||
echo "Dateien in data/: $DATA_FILE_COUNT"
|
||||
fi
|
||||
|
||||
if [ -d "public/uploads" ]; then
|
||||
du -sh "public/uploads"
|
||||
UPLOAD_FILE_COUNT="$(count_files_recursive "public/uploads")"
|
||||
echo "Dateien in public/uploads/: $UPLOAD_FILE_COUNT"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "-- DB Hash (Integritaetsreferenz) --"
|
||||
if [ -f "data/prod.db" ]; then
|
||||
sha256sum "data/prod.db"
|
||||
else
|
||||
echo "data/prod.db nicht gefunden."
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "-- Compose Persistenz-Mounts --"
|
||||
grep -E "^[[:space:]]*-[[:space:]]+\./(data|public/uploads):" "docker-compose.yml" || true
|
||||
echo
|
||||
|
||||
echo "-- Runtime Variablen in Compose --"
|
||||
grep -E "^[[:space:]]*-[[:space:]]+(DATABASE_URL|ADMIN_PASSWORD|TZ|GOTIFY_URL|GOTIFY_APP_TOKEN|OPENROUTER_API_KEY)=" "docker-compose.yml" || true
|
||||
echo
|
||||
|
||||
echo "-- Hinweise --"
|
||||
echo "1) Stelle sicher, dass .env nicht aus Versehen in Transfers/Backups an Dritte gelangt."
|
||||
echo "2) Fuer den Cutover muss die Quell-App vor Final-Sync gestoppt werden."
|
||||
echo "3) Nutze danach scripts/migration-precopy.sh und scripts/migration-cutover.sh."
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Kontrollskript fuer die ersten 24h nach Migration.
|
||||
|
||||
APP_DIR="${APP_DIR:-$(pwd)}"
|
||||
DOMAIN_URL="${DOMAIN_URL:-https://hoerdle.de}"
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-hoerdle}"
|
||||
|
||||
cd "$APP_DIR"
|
||||
|
||||
echo "== Hördle Post-Migration Check =="
|
||||
echo "App dir: $APP_DIR"
|
||||
echo "Domain: $DOMAIN_URL"
|
||||
echo
|
||||
|
||||
echo "-- Container Status --"
|
||||
docker compose ps
|
||||
echo
|
||||
|
||||
echo "-- Health Endpoint --"
|
||||
curl -fsS "$DOMAIN_URL/api/daily" >/dev/null
|
||||
echo "OK: /api/daily erreichbar"
|
||||
echo
|
||||
|
||||
echo "-- Fehlerlogs (24h) --"
|
||||
docker compose logs --since=24h "$CONTAINER_NAME" 2>&1 | grep -Ei "(error|exception|fatal|panic)" || true
|
||||
echo
|
||||
|
||||
echo "-- Backup Testlauf --"
|
||||
./scripts/backup-persistence.sh
|
||||
echo
|
||||
|
||||
echo "Postcheck abgeschlossen."
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Vorabkopie der persistenten Daten von Quelle -> Ziel (App bleibt online).
|
||||
# Auf dem Quellsystem ausfuehren.
|
||||
|
||||
TARGET_HOST="${TARGET_HOST:-root@10.0.0.19}"
|
||||
TARGET_APP_DIR="${TARGET_APP_DIR:-/opt/hoerdle}"
|
||||
SOURCE_APP_DIR="${SOURCE_APP_DIR:-$(pwd)}"
|
||||
SSH_OPTS="${SSH_OPTS:-}"
|
||||
|
||||
cd "$SOURCE_APP_DIR"
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Fehlend: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd rsync
|
||||
require_cmd ssh
|
||||
require_cmd sha256sum
|
||||
require_cmd find
|
||||
|
||||
echo "== Hördle Precopy =="
|
||||
echo "Quelle: $(pwd)"
|
||||
echo "Ziel: $TARGET_HOST:$TARGET_APP_DIR"
|
||||
echo
|
||||
|
||||
for d in data public/uploads; do
|
||||
if [ ! -d "$d" ]; then
|
||||
echo "Pfad fehlt: $d"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "-- Zielordner sicherstellen --"
|
||||
ssh $SSH_OPTS "$TARGET_HOST" "mkdir -p '$TARGET_APP_DIR/data' '$TARGET_APP_DIR/public/uploads'"
|
||||
echo
|
||||
|
||||
echo "-- Vorabkopie data/ --"
|
||||
rsync -aHAX --numeric-ids --delete \
|
||||
-e "ssh $SSH_OPTS" \
|
||||
"./data/" "$TARGET_HOST:$TARGET_APP_DIR/data/"
|
||||
echo
|
||||
|
||||
echo "-- Vorabkopie public/uploads/ --"
|
||||
rsync -aHAX --numeric-ids --delete \
|
||||
-e "ssh $SSH_OPTS" \
|
||||
"./public/uploads/" "$TARGET_HOST:$TARGET_APP_DIR/public/uploads/"
|
||||
echo
|
||||
|
||||
echo "-- Verifikation (Dateien/Groesse) --"
|
||||
LOCAL_DATA_COUNT="$(find data -type f 2>/dev/null | wc -l)"
|
||||
REMOTE_DATA_COUNT="$(ssh $SSH_OPTS "$TARGET_HOST" "cd '$TARGET_APP_DIR' && find data -type f 2>/dev/null | wc -l")"
|
||||
echo "data files local=$LOCAL_DATA_COUNT remote=$REMOTE_DATA_COUNT"
|
||||
|
||||
LOCAL_UPLOAD_COUNT="$(find public/uploads -type f 2>/dev/null | wc -l)"
|
||||
REMOTE_UPLOAD_COUNT="$(ssh $SSH_OPTS "$TARGET_HOST" "cd '$TARGET_APP_DIR' && find public/uploads -type f 2>/dev/null | wc -l")"
|
||||
echo "uploads files local=$LOCAL_UPLOAD_COUNT remote=$REMOTE_UPLOAD_COUNT"
|
||||
|
||||
LOCAL_DATA_SIZE="$(du -sb data | awk '{print $1}')"
|
||||
REMOTE_DATA_SIZE="$(ssh $SSH_OPTS "$TARGET_HOST" "du -sb '$TARGET_APP_DIR/data' | awk '{print \$1}'")"
|
||||
echo "data bytes local=$LOCAL_DATA_SIZE remote=$REMOTE_DATA_SIZE"
|
||||
|
||||
LOCAL_UPLOAD_SIZE="$(du -sb public/uploads | awk '{print $1}')"
|
||||
REMOTE_UPLOAD_SIZE="$(ssh $SSH_OPTS "$TARGET_HOST" "du -sb '$TARGET_APP_DIR/public/uploads' | awk '{print \$1}'")"
|
||||
echo "uploads bytes local=$LOCAL_UPLOAD_SIZE remote=$REMOTE_UPLOAD_SIZE"
|
||||
echo
|
||||
|
||||
if [ -f "data/prod.db" ]; then
|
||||
echo "-- DB Hash Vergleich (Precopy) --"
|
||||
LOCAL_HASH="$(sha256sum data/prod.db | awk '{print $1}')"
|
||||
REMOTE_HASH="$(ssh $SSH_OPTS "$TARGET_HOST" "sha256sum '$TARGET_APP_DIR/data/prod.db' | awk '{print \$1}'")"
|
||||
echo "prod.db sha256 local=$LOCAL_HASH"
|
||||
echo "prod.db sha256 remote=$REMOTE_HASH"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Precopy abgeschlossen. Fuer konsistente Enddaten jetzt Cutover ausfuehren."
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Dieses Skript auf dem ZIEL-LXC ausfuehren.
|
||||
# Es richtet die Ordnerstruktur und Preflight-Checks fuer den Hördle-Betrieb ein.
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/hoerdle}"
|
||||
APP_USER="${APP_USER:-docker}"
|
||||
APP_GROUP="${APP_GROUP:-docker}"
|
||||
|
||||
echo "== Prepare target LXC for Hördle =="
|
||||
echo "APP_DIR=$APP_DIR"
|
||||
echo "APP_USER=$APP_USER"
|
||||
echo
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Fehlend: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd docker
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "docker compose Plugin fehlt."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "-- Docker Version --"
|
||||
docker --version
|
||||
docker compose version
|
||||
echo
|
||||
|
||||
echo "-- Verzeichnisse vorbereiten --"
|
||||
mkdir -p "$APP_DIR"/{data,public/uploads,logs,backups}
|
||||
chown -R "$APP_USER:$APP_GROUP" "$APP_DIR"
|
||||
chmod 755 "$APP_DIR"
|
||||
chmod 755 "$APP_DIR/data" "$APP_DIR/public" "$APP_DIR/public/uploads"
|
||||
echo "Ordner vorbereitet."
|
||||
echo
|
||||
|
||||
echo "-- Speichercheck --"
|
||||
df -h "$APP_DIR"
|
||||
echo
|
||||
|
||||
echo "-- Netzwerkcheck --"
|
||||
if ! docker network inspect hoerdle_default >/dev/null 2>&1; then
|
||||
echo "Docker-Netzwerk hoerdle_default fehlt, wird erstellt."
|
||||
docker network create hoerdle_default >/dev/null
|
||||
fi
|
||||
echo "Netzwerk hoerdle_default bereit."
|
||||
echo
|
||||
|
||||
echo "-- .env Vorlage --"
|
||||
if [ ! -f "$APP_DIR/.env" ]; then
|
||||
if [ -f "$APP_DIR/.env.proxmox.example" ]; then
|
||||
cp "$APP_DIR/.env.proxmox.example" "$APP_DIR/.env"
|
||||
echo ".env aus .env.proxmox.example erstellt."
|
||||
elif [ -f "$APP_DIR/.env.example" ]; then
|
||||
cp "$APP_DIR/.env.example" "$APP_DIR/.env"
|
||||
echo ".env aus .env.example erstellt."
|
||||
else
|
||||
echo "Keine Env-Vorlage gefunden. Bitte .env manuell erstellen."
|
||||
fi
|
||||
else
|
||||
echo ".env existiert bereits, kein Override."
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "-- Abschluss --"
|
||||
echo "Target-LXC vorbereitet. Nächster Schritt: Daten-Vorabkopie aus der Quelle."
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Rollback-Hilfsskript: stoppt Ziel-Container und zeigt klare Operator-Hinweise.
|
||||
# Die eigentliche Rückleitung des Traffics erfolgt im Nginx Proxy Manager.
|
||||
|
||||
TARGET_HOST="${TARGET_HOST:-root@10.0.0.19}"
|
||||
TARGET_APP_DIR="${TARGET_APP_DIR:-/opt/hoerdle}"
|
||||
TARGET_COMPOSE_FILE="${TARGET_COMPOSE_FILE:-docker-compose.yml}"
|
||||
SSH_OPTS="${SSH_OPTS:-}"
|
||||
|
||||
echo "== Hördle Rollback-Hilfe =="
|
||||
echo "Zielhost: $TARGET_HOST"
|
||||
echo
|
||||
|
||||
echo "-- Ziel-App stoppen --"
|
||||
ssh $SSH_OPTS "$TARGET_HOST" "cd '$TARGET_APP_DIR' && docker compose -f '$TARGET_COMPOSE_FILE' down"
|
||||
echo
|
||||
|
||||
echo "Naechste Schritte:"
|
||||
echo "1) NPM Proxy Host sofort wieder auf alte VPS-Instanz umstellen."
|
||||
echo "2) Externen Smoke-Test auf alter Instanz durchfuehren."
|
||||
echo "3) Fehleranalyse im Ziel-LXC (docker compose logs)."
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Smoke-Tests nach dem Umschalten auf Proxmox/NPM.
|
||||
# Kann lokal oder auf beliebigem Host mit curl ausgefuehrt werden.
|
||||
|
||||
BASE_URL="${1:-https://hoerdle.de}"
|
||||
EXPECTED_STATUS="${EXPECTED_STATUS:-200}"
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Fehlend: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd curl
|
||||
require_cmd grep
|
||||
|
||||
echo "== Hördle Smoke-Test =="
|
||||
echo "BASE_URL=$BASE_URL"
|
||||
echo
|
||||
|
||||
check_http() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local code
|
||||
|
||||
code="$(curl -sS -o /dev/null -w "%{http_code}" "$url" || true)"
|
||||
if [ "$code" = "$EXPECTED_STATUS" ]; then
|
||||
echo "[OK] $name -> $code"
|
||||
else
|
||||
echo "[FAIL] $name -> $code (erwartet: $EXPECTED_STATUS)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_body_contains() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local needle="$3"
|
||||
|
||||
if curl -fsS "$url" | grep -q "$needle"; then
|
||||
echo "[OK] $name enthält '$needle'"
|
||||
else
|
||||
echo "[FAIL] $name enthält '$needle' nicht"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
|
||||
check_http "Homepage" "$BASE_URL/" || FAILED=1
|
||||
check_http "Daily API" "$BASE_URL/api/daily" || FAILED=1
|
||||
|
||||
# Prüft, ob Daily API als JSON zurückkommt.
|
||||
check_body_contains "Daily API" "$BASE_URL/api/daily" "{" || FAILED=1
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "Smoke-Test erfolgreich."
|
||||
else
|
||||
echo "Smoke-Test fehlgeschlagen."
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user