update.sh
Rama main · réplica de lectura del repositorio original.
#!/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 -- <datei> 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" <<WIEDERHERSTELLEN
# Wiederherstellung dieser Sicherung ($STAMP)
Datenbank (ersetzt den aktuellen Inhalt):
docker compose up -d rag-db
gunzip -c postgres.sql.gz | docker compose exec -T rag-db psql -U $DB_USER -d $DB_NAME
Originaldateien und Uploads:
docker run --rm -v ${APP_VOLUME}:/ziel -v "\$PWD":/quelle:ro \\
alpine:3 sh -c "rm -rf /ziel/* && tar xzf /quelle/app-data.tar.gz -C /ziel"
Konfiguration:
cp env.backup ../../.env
Elasticsearch muss nicht gesichert werden - der Index laesst sich im Admin-UI
ueber "Reindex starten" vollstaendig aus PostgreSQL neu aufbauen.
WIEDERHERSTELLEN
ok "Anleitung: $BACKUP_DIR/WIEDERHERSTELLEN.md"
else
info "Sicherung uebersprungen (--skip-backup)"
warn "Bei einem Fehlschlag gibt es keinen Rueckweg"
fi
# --- Code aktualisieren ------------------------------------------------------
if [ "$DO_PULL" = "1" ]; then
info "Code aktualisieren"
VORHER="$(git rev-parse --short HEAD)"
# --ff-only: kein stiller Merge-Commit, kein halb aufgeloester Konflikt.
if ! git pull --ff-only; then
fail "git pull fehlgeschlagen (kein Fast-Forward moeglich)"
echo " Lokale Commits mit 'git log --oneline origin/HEAD..HEAD' pruefen" >&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