#!/usr/bin/env bash # # Aktualisiert eine laufende Installation ohne Datenverlust. # # Reihenfolge ist Absicht: erst sichern, dann ziehen, dann bauen, dann starten. # Bricht ein Schritt ab, ist die Sicherung schon geschrieben und der alte Stand # laeuft entweder noch oder laesst sich daraus wiederherstellen. # # Nicht verwendet wird "docker compose down -v" - das loescht die Volumes und # damit Datenbank und Originaldateien. # # ./update.sh Sicherung, git pull, Neubau, Start, Pruefung # ./update.sh --skip-backup ohne Sicherung (nur wenn anderweitig gesichert) # ./update.sh --no-pull nur neu bauen und starten, ohne git pull # ./update.sh --stash lokale Aenderungen beiseitelegen und danach behalten # ./update.sh --help set -euo pipefail cd "$(dirname "$0")" SKIP_BACKUP=0 DO_PULL=1 STASH=0 STASH_GESETZT=0 STAMP_VORAB="$(date +%Y%m%d-%H%M%S)" while [ $# -gt 0 ]; do case "$1" in --skip-backup) SKIP_BACKUP=1 ;; --no-pull) DO_PULL=0 ;; --stash) STASH=1 ;; --help|-h) sed -n '3,16p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "Unbekannte Option: $1 (siehe --help)" >&2 exit 2 ;; esac shift done info() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } ok() { printf ' \033[32mOK\033[0m %s\n' "$*"; } warn() { printf ' \033[33mHinweis\033[0m %s\n' "$*"; } fail() { printf ' \033[31mFEHLER\033[0m %s\n' "$*" >&2; } # Bricht irgendein Schritt ab, soll niemand raten muessen, in welchem Zustand # die Installation ist. Ohne diese Meldung endete ein Fehlschlag mit einem # nackten Build-Log und der Frage, ob die Daten noch da sind. BACKUP_DIR="" abschluss() { code=$? [ "$code" = "0" ] && exit 0 printf '\n\033[31m==> Abgebrochen\033[0m\n' >&2 # Wurde vorher gestasht, darf der Abbruch den Arbeitsbaum nicht veraendert # zuruecklassen - sonst sind die eigenen Aenderungen scheinbar verschwunden. if [ "${STASH_GESETZT:-0}" = "1" ]; then if git stash pop >/dev/null 2>&1; then echo " Beiseitegelegte Aenderungen wurden zurueckgeholt." >&2 else echo " ACHTUNG: beiseitegelegte Aenderungen liegen noch im Stash." >&2 echo " Zurueckholen mit: git stash pop" >&2 fi fi echo " Die laufende Installation wurde nicht veraendert: es gab kein 'down'," >&2 echo " keine Volume-Aenderung und keine Migration ausserhalb des Starts." >&2 if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then echo " Sicherung liegt unter: $BACKUP_DIR" >&2 fi echo " Laufen die Container noch? 'docker compose ps'" >&2 exit "$code" } trap abschluss EXIT # --- Voraussetzungen --------------------------------------------------------- info "Voraussetzungen" for tool in git docker; do command -v "$tool" >/dev/null 2>&1 || { fail "$tool nicht gefunden"; exit 1; } done docker compose version >/dev/null 2>&1 || { fail "'docker compose' nicht verfuegbar"; exit 1; } [ -f docker-compose.yml ] || { fail "docker-compose.yml fehlt - Skript im Projektverzeichnis ausfuehren"; exit 1; } [ -f .env ] || { fail ".env fehlt - ohne Konfiguration startet der Stack nicht"; exit 1; } ok "git, docker compose, docker-compose.yml und .env vorhanden" # Gebaut wird aus dem Arbeitsbaum, nicht aus dem Git-Index. Eine geloeschte # nachverfolgte Datei laesst also den Build scheitern - unabhaengig davon, ob # gezogen wird. Deshalb wird dieser Fall immer geprueft. GELOESCHT="$(git ls-files --deleted 2>/dev/null || true)" if [ -n "$GELOESCHT" ]; then fail "Diese nachverfolgten Dateien fehlen im Arbeitsverzeichnis:" echo "$GELOESCHT" | sed 's/^/ /' >&2 echo " Zurueckholen mit:" >&2 echo " git checkout -- $(echo "$GELOESCHT" | tr '\n' ' ')" >&2 exit 1 fi # Ein unsauberer Arbeitsbaum und "git pull --ff-only" vertragen sich nicht. # Lieber hier abbrechen als mitten im Update auf einen Konflikt laufen - aber # mit den Befehlen, die tatsaechlich weiterhelfen. if [ "$DO_PULL" = "1" ] && [ -n "$(git status --porcelain 2>/dev/null)" ]; then if [ "$STASH" = "1" ]; then info "Lokale Aenderungen beiseitelegen" git stash push --include-untracked -m "update.sh $STAMP_VORAB" STASH_GESETZT=1 ok "Beiseitegelegt - Rueckholen mit 'git stash pop'" else fail "Es gibt lokale Aenderungen:" git status --short | sed 's/^/ /' >&2 cat >&2 <<'AUSWEG' Moeglichkeiten: ./update.sh --stash Aenderungen beiseitelegen und danach behalten git checkout -- einzelne Aenderung verwerfen git stash push -u alles beiseitelegen (spaeter: git stash pop) ./update.sh --no-pull nur neu bauen, ohne zu ziehen AUSWEG exit 1 fi fi # --- Sicherung --------------------------------------------------------------- STAMP="$STAMP_VORAB" BACKUP_DIR="backups/$STAMP" if [ "$SKIP_BACKUP" = "0" ]; then info "Sicherung nach $BACKUP_DIR" mkdir -p "$BACKUP_DIR" # Zugangsdaten aus .env lesen, sonst die Vorgaben aus docker-compose.yml. DB_URL="$(grep -E '^DATABASE_URL=' .env | head -1 | cut -d= -f2- || true)" DB_USER="$(printf '%s' "$DB_URL" | sed -n 's#^postgres\(ql\)\?://\([^:]*\):.*#\2#p')" DB_NAME="$(printf '%s' "$DB_URL" | sed -n 's#.*/\([^/?]*\)\(?.*\)\?$#\1#p')" DB_USER="${DB_USER:-user}" DB_NAME="${DB_NAME:-rag_db}" if docker compose ps --status running --services 2>/dev/null | grep -qx "rag-db"; then # Ohne --clean/--if-exists, damit die Sicherung auch in eine frische, # leere Datenbank eingespielt werden kann. if docker compose exec -T rag-db pg_dump -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_DIR/postgres.sql.gz"; then ok "Datenbank gesichert ($(du -h "$BACKUP_DIR/postgres.sql.gz" | cut -f1), Benutzer $DB_USER, DB $DB_NAME)" else fail "pg_dump fehlgeschlagen - Update abgebrochen, es wurde nichts veraendert" exit 1 fi else fail "Container rag-db laeuft nicht - ohne laufende Datenbank keine Sicherung" echo " Stack starten ('docker compose up -d rag-db') oder --skip-backup nutzen" >&2 exit 1 fi # Originaldateien und Uploads liegen im Volume app-data und sind nicht aus der # Datenbank rekonstruierbar. Den echten Volume-Namen vom Container erfragen, # er haengt vom Compose-Projektnamen ab. APP_VOLUME="$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/app/data"}}{{.Name}}{{end}}{{end}}' rag-ingestor-app 2>/dev/null || true)" if [ -z "$APP_VOLUME" ]; then APP_VOLUME="$(basename "$PWD" | tr '[:upper:]' '[:lower:]')_app-data" warn "Volume-Name geraten: $APP_VOLUME" fi if docker volume inspect "$APP_VOLUME" >/dev/null 2>&1; then docker run --rm \ -v "$APP_VOLUME":/quelle:ro \ -v "$PWD/$BACKUP_DIR":/ziel \ alpine:3 tar czf /ziel/app-data.tar.gz -C /quelle . 2>/dev/null ok "Originaldateien gesichert ($(du -h "$BACKUP_DIR/app-data.tar.gz" | cut -f1), Volume $APP_VOLUME)" else warn "Volume $APP_VOLUME nicht gefunden - Originaldateien nicht gesichert" fi # Die .env ist nicht im Repository und waere bei einem Fehlgriff weg. cp .env "$BACKUP_DIR/env.backup" ok "Konfiguration (.env) gesichert" cat > "$BACKUP_DIR/WIEDERHERSTELLEN.md" <&2 exit 1 fi NACHHER="$(git rev-parse --short HEAD)" if [ "$VORHER" = "$NACHHER" ]; then ok "Bereits aktuell ($NACHHER)" else ok "$VORHER -> $NACHHER" git log --oneline "$VORHER..$NACHHER" | sed 's/^/ /' fi else info "git pull uebersprungen (--no-pull)" fi # --- Neue Konfigurationsschluessel melden ------------------------------------ if [ -f .env.example ]; then FEHLEND="$(grep -oE '^[A-Z_][A-Z0-9_]*=' .env.example | tr -d '=' | while read -r key; do grep -qE "^${key}=" .env || echo "$key" done)" if [ -n "$FEHLEND" ]; then info "Neue Konfigurationsschluessel" warn "In .env fehlen (es greifen die Vorgaben aus dem Code):" echo "$FEHLEND" | sed 's/^/ /' fi fi # --- Bauen und starten ------------------------------------------------------- info "Images bauen" docker compose build ingestor-app ingestor-worker ragfind ok "Build abgeschlossen" info "Container starten" # Kein "down": die Volumes und damit alle Daten bleiben unangetastet. # Datenbankmigrationen laufen beim Start der Anwendung automatisch. docker compose up -d ok "Container gestartet" # --- Pruefen ----------------------------------------------------------------- info "Pruefung" PORT="$(grep -E '^PORT=' .env | head -1 | cut -d= -f2- || true)" PORT="${PORT:-3311}" BEREIT=0 for _ in $(seq 1 60); do # 401 zaehlt als erreichbar: /api/status verlangt Admin-Auth, die Anwendung # antwortet also bereits. CODE="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "http://localhost:${PORT}/api/status" 2>/dev/null || echo 000)" case "$CODE" in 200|401) BEREIT=1; break ;; esac sleep 2 done if [ "$BEREIT" = "1" ]; then ok "Anwendung antwortet auf Port $PORT" else fail "Anwendung antwortet nach 120 s nicht" echo " Logs: docker compose logs --tail 50 ingestor-app" >&2 exit 1 fi NICHT_OBEN="$(docker compose ps --format '{{.Service}} {{.State}}' 2>/dev/null | awk '$2 != "running" {print $1}')" if [ -n "$NICHT_OBEN" ]; then fail "Diese Dienste laufen nicht:" echo "$NICHT_OBEN" | sed 's/^/ /' >&2 exit 1 fi ok "Alle Dienste laufen" # Migrationen laufen beim Start; ein Fehler dabei wuerde die Anwendung stoppen, # trotzdem hier gegenpruefen, ob die juengste Migration angekommen ist. LETZTE_MIGRATION="$(ls migrations/*.sql 2>/dev/null | sort | tail -1)" if [ -n "$LETZTE_MIGRATION" ]; then ok "Migrationen bis $(basename "$LETZTE_MIGRATION") im Image enthalten und beim Start angewandt" fi if [ "$STASH_GESETZT" = "1" ]; then info "Beiseitegelegte Aenderungen zurueckholen" if git stash pop; then ok "Zurueckgeholt" else warn "Konflikt beim Zurueckholen - die Aenderungen liegen weiter in 'git stash list'" fi fi info "Fertig" if [ "$SKIP_BACKUP" = "0" ]; then echo " Sicherung: $BACKUP_DIR" fi cat <<'HINWEIS' Manuelle Schritte, falls das Update Suche oder Embeddings betrifft: - Admin-UI -> "Reindex starten" (nach Aenderungen am Elasticsearch-Mapping) - Admin-UI -> "Re-Embedding starten" (nach Aenderungen am Embedding-Input) Beides steht im jeweiligen Release-Hinweis bzw. in optimierungsplan.md. HINWEIS