feat: update.sh für den Betrieb unter Linux

Erst sichern, dann anfassen: schlägt die Sicherung fehl, bricht das Skript ab
statt weiterzumachen. Geht danach etwas schief, nennt es den konkreten Weg
zurück — mit dem Commit, zu dem man zurückspringt, und der Datei, die man
einspielt, nicht bloß dem Rat, ein Backup zu haben.

Lokale Änderungen im Arbeitsverzeichnis werden erkannt und nicht überfahren.
Die Erreichbarkeit wird über den veröffentlichten Port geprüft und, falls der
nicht nach außen geht, über den Healthcheck des Containers.

Das Ausführbar-Bit steckt im Repository — nach dem Klonen ist es startklar.
This commit is contained in:
Florian Wilde
2026-09-01 22:46:36 +02:00
parent 9ee8664233
commit 3592cf51e6
8 changed files with 321 additions and 8 deletions
+1
View File
@@ -4,6 +4,7 @@ node_modules
.env
.env.local
data
backups
docs/screenshots
*.log
test-results
+3
View File
@@ -27,6 +27,9 @@ AUTH_SECRET=bitte-durch-zufallswert-ersetzen
# Ablage für Bild-Cache und sonstige Laufzeitdaten.
DATA_DIR=/app/data
# Port, auf dem Flimmer auf dem Host lauscht.
APP_PORT=3000
# Zeitzone für Kalender, Statistiken und geplante Läufe
TZ=Europe/Berlin
+1
View File
@@ -11,6 +11,7 @@ next-env.d.ts
# Produktion / Laufzeit
data/
backups/
*.tsbuildinfo
# Umgebung — niemals committen!
+28 -1
View File
@@ -7,6 +7,32 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/).
## [Unveröffentlicht]
## [0.7.1] — 2026-09-01
### Hinzugefügt
- **`update.sh`** für den Betrieb unter Linux — ausführbar im Repository
hinterlegt, also direkt nach dem Klonen startklar. Der Ablauf: Datenbank
sichern, Änderungen holen, Image bauen, neu starten, warten bis die Instanz
wieder antwortet.
Grundhaltung: erst sichern, dann anfassen. Schlägt die Sicherung fehl, bricht
das Skript ab, statt weiterzumachen. Geht danach etwas schief, nennt es den
**konkreten** Weg zurück — mit dem Commit, zu dem man zurückspringt, und der
Datei, die man einspielt, nicht bloß dem Rat, ein Backup zu haben.
Weitere Eigenheiten:
- Lokale Änderungen im Arbeitsverzeichnis werden erkannt; `git pull`
unterbleibt dann, statt sie zu überfahren.
- Erreichbarkeit wird über den veröffentlichten Port geprüft und, falls der
nicht nach außen geht (Reverse Proxy im selben Docker-Netz), über den
Healthcheck des Containers.
- Alte Sicherungen werden aufgeräumt, die letzten zehn bleiben (`--behalte N`).
- Kommt mit `docker compose` und dem alten `docker-compose` zurecht.
- `APP_PORT` in der `.env.example` — der Port stand bisher nur in der
Compose-Datei als Vorgabewert.
## [0.7.0] — 2026-09-01
„Hab ich" — Titel, die in der eigenen Sammlung liegen. Dazu die Nacharbeiten
@@ -321,7 +347,8 @@ Erste lauffähige Etappe: Gerüst, Marke, Datenmodell und Anmeldung.
dass weitere Sprachen als zusätzliche Datei dazukommen können.
- **Seed-Skript** mit Demo-Konten sowie README und Design-Dokumentation.
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.7.0...HEAD
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.7.1...HEAD
[0.7.1]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.7.1
[0.7.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.7.0
[0.6.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.6.0
[0.5.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.5.0
+23 -6
View File
@@ -114,14 +114,31 @@ flimmer.example.com {
```bash
cd flimmer
git pull
docker compose build
docker compose up -d
./update.sh
```
Datenbankmigrationen laufen automatisch beim Start des Containers — ein eigener
Handgriff ist nicht nötig. **Vor** einem Update ein Backup zu ziehen ist
trotzdem eine gute Angewohnheit.
Das Skript sichert zuerst die Datenbank, holt die Änderungen, baut das Image neu
und wartet, bis die Instanz wieder antwortet. Schlägt etwas fehl, nennt es den
konkreten Weg zurück — mit dem Befehl und der Sicherungsdatei, nicht bloß dem
Rat, ein Backup zu haben.
```
./update.sh --ohne-backup ohne Datenbanksicherung
./update.sh --ohne-pull nur neu bauen und starten
./update.sh --behalte 20 wie viele Sicherungen aufgehoben werden (Standard: 10)
./update.sh --hilfe
```
Die Sicherungen landen unter `backups/` und sind gzip-komprimierte `pg_dump`s.
Von Hand geht es genauso:
```bash
git pull && docker compose build && docker compose up -d
```
Datenbankmigrationen laufen in beiden Fällen automatisch beim Start des
Containers — ein eigener Handgriff ist nicht nötig.
## Backup
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "flimmer",
"version": "0.7.0",
"version": "0.7.1",
"private": true,
"description": "Flimmer — selbst hostbarer Serien- und Film-Tracker",
"license": "MIT",
+25
View File
@@ -0,0 +1,25 @@
# Flimmer 0.7.1
### Hinzugefügt
- **`update.sh`** für den Betrieb unter Linux — ausführbar im Repository
hinterlegt, also direkt nach dem Klonen startklar. Der Ablauf: Datenbank
sichern, Änderungen holen, Image bauen, neu starten, warten bis die Instanz
wieder antwortet.
Grundhaltung: erst sichern, dann anfassen. Schlägt die Sicherung fehl, bricht
das Skript ab, statt weiterzumachen. Geht danach etwas schief, nennt es den
**konkreten** Weg zurück — mit dem Commit, zu dem man zurückspringt, und der
Datei, die man einspielt, nicht bloß dem Rat, ein Backup zu haben.
Weitere Eigenheiten:
- Lokale Änderungen im Arbeitsverzeichnis werden erkannt; `git pull`
unterbleibt dann, statt sie zu überfahren.
- Erreichbarkeit wird über den veröffentlichten Port geprüft und, falls der
nicht nach außen geht (Reverse Proxy im selben Docker-Netz), über den
Healthcheck des Containers.
- Alte Sicherungen werden aufgeräumt, die letzten zehn bleiben (`--behalte N`).
- Kommt mit `docker compose` und dem alten `docker-compose` zurecht.
- `APP_PORT` in der `.env.example` — der Port stand bisher nur in der
Compose-Datei als Vorgabewert.
Executable
+239
View File
@@ -0,0 +1,239 @@
#!/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.
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."
GESUND=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
sleep 2
done
if [ "$GESUND" -eq 1 ]; then
ANTWORT=$(curl -fsS "http://127.0.0.1:${PORT}/api/health" 2>/dev/null || true)
gut "Flimmer antwortet auf Port ${PORT}."
leise "$ANTWORT"
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
[ -n "$SICHERUNG" ] && leise "Sicherung: $SICHERUNG"
exit 0
fi
# --- Fehlschlag: konkreter Weg zurück ----------------------------------------
printf '\n%s✗ Die Instanz antwortet nach zwei Minuten nicht.%s\n\n' "$FEHLER" "$AUS" >&2
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