#!/usr/bin/env bash # # Flimmer aktualisieren. # # ./update.sh sichern, holen, bauen, neu starten # ./update.sh --ohne-backup ohne Datenbanksicherung # ./update.sh --ohne-pull nur neu bauen und starten (kein git pull) # ./update.sh --hilfe # # Grundhaltung: Erst sichern, dann anfassen. Bricht etwas ab, steht am Ende, # wie der Weg zurück aussieht — mit dem konkreten Befehl und der konkreten # Sicherungsdatei, nicht bloß dem Rat, ein Backup zu haben. set -euo pipefail # --- Ausgabe ----------------------------------------------------------------- if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then AKZENT=$'\033[38;5;214m'; GUT=$'\033[32m'; WARN=$'\033[33m' FEHLER=$'\033[31m'; LEISE=$'\033[2m'; AUS=$'\033[0m' else AKZENT=""; GUT=""; WARN=""; FEHLER=""; LEISE=""; AUS="" fi schritt() { printf '\n%s▸ %s%s\n' "$AKZENT" "$1" "$AUS"; } info() { printf ' %s\n' "$1"; } leise() { printf ' %s%s%s\n' "$LEISE" "$1" "$AUS"; } gut() { printf ' %s✓ %s%s\n' "$GUT" "$1" "$AUS"; } warnung() { printf ' %s! %s%s\n' "$WARN" "$1" "$AUS"; } abbruch() { printf '\n%s✗ %s%s\n' "$FEHLER" "$1" "$AUS" >&2; exit 1; } hilfe() { cat <<'ENDE' Flimmer aktualisieren ./update.sh [OPTIONEN] Ablauf: Datenbank sichern → Änderungen holen → Image bauen → neu starten → warten, bis die Instanz wieder antwortet. Die Datenbankmigrationen spielt der Container beim Start selbst ein. Geprüft wird zuerst http://127.0.0.1:APP_PORT/api/health. Ist der Port gar nicht auf dem Host veröffentlicht — üblich hinter einem Reverse Proxy —, zählt stattdessen der Healthcheck des Containers. Optionen: --ohne-backup Keine Sicherung anlegen (nicht empfohlen) --ohne-pull Nichts aus dem Repository holen, nur neu bauen und starten --behalte N Wie viele Sicherungen aufgehoben werden (Standard: 10) --hilfe, -h Diese Übersicht Die Sicherungen liegen unter backups/ und sind gzip-komprimierte pg_dumps. ENDE } # --- Optionen ---------------------------------------------------------------- MIT_BACKUP=1 MIT_PULL=1 BEHALTE=10 while [ $# -gt 0 ]; do case "$1" in --ohne-backup) MIT_BACKUP=0 ;; --ohne-pull) MIT_PULL=0 ;; --behalte) shift; BEHALTE="${1:-10}" ;; --hilfe|-h) hilfe; exit 0 ;; *) abbruch "Unbekannte Option: $1 (./update.sh --hilfe)" ;; esac shift done # --- Voraussetzungen --------------------------------------------------------- cd "$(dirname "$0")" [ -f docker-compose.yml ] || abbruch "Hier liegt keine docker-compose.yml. Das Skript gehört ins Flimmer-Verzeichnis." [ -f .env ] || abbruch "Es fehlt die .env. Lege sie an: cp .env.example .env" command -v docker >/dev/null 2>&1 || abbruch "docker ist nicht installiert oder nicht im Pfad." if docker compose version >/dev/null 2>&1; then COMPOSE="docker compose" elif command -v docker-compose >/dev/null 2>&1; then COMPOSE="docker-compose" else abbruch "Weder 'docker compose' noch 'docker-compose' gefunden." fi docker info >/dev/null 2>&1 || abbruch "Der Docker-Dienst antwortet nicht. Läuft er?" # Werte aus der .env lesen, ohne sie auszuführen. env_wert() { local schluessel="$1" standard="${2:-}" local zeile zeile=$(grep -E "^[[:space:]]*${schluessel}=" .env 2>/dev/null | tail -1 || true) if [ -z "$zeile" ]; then printf '%s' "$standard"; return; fi zeile="${zeile#*=}" zeile="${zeile%\"}"; zeile="${zeile#\"}" zeile="${zeile%\'}"; zeile="${zeile#\'}" printf '%s' "$zeile" } DB_USER=$(env_wert POSTGRES_USER flimmer) DB_NAME=$(env_wert POSTGRES_DB flimmer) PORT=$(env_wert APP_PORT 3000) printf '%s\n' "Flimmer aktualisieren" leise "Verzeichnis: $(pwd)" leise "Compose: $COMPOSE" # --- Stand festhalten -------------------------------------------------------- IST_GIT=0 VORHER_REF="" if [ -d .git ] && command -v git >/dev/null 2>&1; then IST_GIT=1 VORHER_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unbekannt") fi VORHER_VERSION=$(grep -m1 '"version"' package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/' || echo "?") leise "Aktuell: Version $VORHER_VERSION${VORHER_REF:+ (${VORHER_REF})}" # --- Sicherung --------------------------------------------------------------- SICHERUNG="" if [ "$MIT_BACKUP" -eq 1 ]; then schritt "Datenbank sichern" # -q gibt es in beiden Compose-Fassungen; --status erst ab v2. if [ -z "$($COMPOSE ps -q db 2>/dev/null)" ]; then warnung "Die Datenbank läuft nicht — es gibt also auch nichts zu sichern." else mkdir -p backups SICHERUNG="backups/flimmer-$(date +%Y-%m-%d-%H%M%S).sql.gz" if $COMPOSE exec -T db pg_dump -U "$DB_USER" -d "$DB_NAME" --clean --if-exists \ | gzip > "$SICHERUNG" 2>/dev/null && [ -s "$SICHERUNG" ]; then gut "$SICHERUNG ($(du -h "$SICHERUNG" | cut -f1))" else rm -f "$SICHERUNG" SICHERUNG="" abbruch "Die Sicherung ist fehlgeschlagen. Abbruch — ohne Sicherung wird hier nichts aktualisiert. Wenn du es trotzdem willst: ./update.sh --ohne-backup" fi # Ältere Sicherungen aufräumen, die jüngsten bleiben. if [ "$BEHALTE" -gt 0 ]; then ANZAHL=$(ls -1 backups/flimmer-*.sql.gz 2>/dev/null | wc -l) if [ "$ANZAHL" -gt "$BEHALTE" ]; then ls -1t backups/flimmer-*.sql.gz | tail -n +$((BEHALTE + 1)) | xargs -r rm -- leise "Ältere Sicherungen entfernt, die letzten $BEHALTE bleiben." fi fi fi else warnung "Ohne Sicherung — auf eigenes Risiko." fi # --- Änderungen holen -------------------------------------------------------- if [ "$MIT_PULL" -eq 1 ]; then schritt "Änderungen holen" if [ "$IST_GIT" -eq 0 ]; then warnung "Kein Git-Repository — übersprungen." elif [ -n "$(git status --porcelain 2>/dev/null)" ]; then warnung "Es gibt lokale Änderungen; git pull wird übersprungen." leise "Ansehen mit: git status" else ZWEIG=$(git rev-parse --abbrev-ref HEAD) if git pull --ff-only 2>&1 | sed 's/^/ /'; then NACHHER_REF=$(git rev-parse --short HEAD) if [ "$NACHHER_REF" = "$VORHER_REF" ]; then gut "Schon auf dem neuesten Stand ($ZWEIG)." else gut "$VORHER_REF → $NACHHER_REF" fi else abbruch "git pull ist fehlgeschlagen. Vermutlich sind Zweig und Gegenstelle auseinandergelaufen." fi fi fi NEUE_VERSION=$(grep -m1 '"version"' package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/' || echo "?") # --- Bauen und starten ------------------------------------------------------- schritt "Image bauen" info "Das dauert beim ersten Mal einige Minuten." $COMPOSE build 2>&1 | sed 's/^/ /' || abbruch "Der Build ist fehlgeschlagen. Die laufende Instanz ist unangetastet." gut "Gebaut." schritt "Neu starten" $COMPOSE up -d 2>&1 | sed 's/^/ /' || abbruch "Der Start ist fehlgeschlagen. Log ansehen: $COMPOSE logs -f app" gut "Container laufen." # --- Warten, bis die Instanz antwortet --------------------------------------- schritt "Auf die Instanz warten" info "Die Datenbankmigrationen spielt der Container beim Start selbst ein." # Der Container meldet seinen Zustand selbst — der HEALTHCHECK im Image fragt # /api/health von innen ab. Das ist der verlässlichere Weg: hinter einem # Reverse Proxy ist der App-Port oft gar nicht auf dem Host veröffentlicht, # und dann sagt ein fehlschlagendes curl nichts über die Instanz aus. # Gibt immer 0 zurück: die Funktion läuft auch im Fehlerpfad, und unter # `set -e` dürfte eine Auskunft über den Zustand nicht selbst das Skript # abbrechen, bevor der Weg zurück ausgegeben wurde. container_zustand() { local id id=$($COMPOSE ps -q app 2>/dev/null || true) if [ -z "$id" ]; then printf 'kein-container' return 0 fi # Ohne HEALTHCHECK im Image ist .State.Health leer — dann bleibt der # Laufzustand (running/exited) als grobe Auskunft. docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}ohne-healthcheck:{{.State.Status}}{{end}}' \ "$id" 2>/dev/null || printf 'unbekannt' return 0 } GESUND=0 UEBER_CONTAINER=0 for _ in $(seq 1 60); do if curl -fsS "http://127.0.0.1:${PORT}/api/health" >/dev/null 2>&1; then GESUND=1 break fi if [ "$(container_zustand)" = "healthy" ]; then GESUND=1 UEBER_CONTAINER=1 break fi sleep 2 done if [ "$GESUND" -eq 1 ]; then if [ "$UEBER_CONTAINER" -eq 1 ]; then gut "Der Container meldet sich gesund." leise "Auf Port ${PORT} war nichts zu erreichen — das ist normal, wenn die App" leise "nur über einen Reverse Proxy erreichbar ist und den Port nicht veröffentlicht." else ANTWORT=$(curl -fsS "http://127.0.0.1:${PORT}/api/health" 2>/dev/null || true) gut "Flimmer antwortet auf Port ${PORT}." leise "$ANTWORT" fi printf '\n%s✓ Fertig — Version %s%s\n' "$GUT" "$NEUE_VERSION" "$AUS" if [ "$VORHER_VERSION" != "$NEUE_VERSION" ]; then info "Was sich geändert hat, steht in CHANGELOG.md." fi if [ -n "$SICHERUNG" ]; then leise "Sicherung: $SICHERUNG" fi exit 0 fi # --- Fehlschlag: konkreter Weg zurück ---------------------------------------- printf '\n%s✗ Die Instanz antwortet nach zwei Minuten nicht.%s\n\n' "$FEHLER" "$AUS" >&2 # Der Container-Zustand grenzt ein, wo es klemmt — ohne ihn sähe ein nicht # veröffentlichter Port genauso aus wie eine abgestürzte App. ZUSTAND=$(container_zustand) case "$ZUSTAND" in kein-container) info "Es läuft gar kein app-Container. Der Start ist offenbar fehlgeschlagen." ;; starting) info "Der Container startet noch. Möglicherweise dauern die Migrationen länger —" info "sieh im Log nach und gib ihm etwas Zeit." ;; unhealthy) info "Der Container läuft, meldet sich aber als nicht gesund. Der Fehler liegt" info "also in der Anwendung, nicht am Port." ;; ohne-healthcheck:running) info "Der Container läuft, das Image bringt aber keinen Healthcheck mit (ältere" info "Fassung). Ob die App wirklich antwortet, verrät nur das Log." ;; ohne-healthcheck:*) info "Der Container ist nicht gelaufen: ${ZUSTAND#ohne-healthcheck:}" ;; *) info "Container-Zustand: ${ZUSTAND}" ;; esac printf '\n' info "Log ansehen:" printf ' %s logs --tail 50 app\n\n' "$COMPOSE" if [ -n "$SICHERUNG" ] || [ -n "$VORHER_REF" ]; then info "Weg zurück:" if [ "$IST_GIT" -eq 1 ] && [ -n "$VORHER_REF" ]; then printf ' git checkout %s && %s up -d --build\n' "$VORHER_REF" "$COMPOSE" fi if [ -n "$SICHERUNG" ]; then printf ' gunzip -c %s | %s exec -T db psql -U %s -d %s\n' \ "$SICHERUNG" "$COMPOSE" "$DB_USER" "$DB_NAME" fi printf '\n' fi exit 1