fix(scripts): Einrichtung und Update ohne Node
CI / pruefen (push) Has been cancelled

Ein reiner Docker-Host hat kein Node installiert. Die Anleitung verlangte
trotzdem "node scripts/setup.mjs", und update.sh las die Versionsnummer mit
"node -p" - beides waere dort gescheitert.

scripts/setup.sh macht dasselbe wie die .mjs-Fassung in reinem POSIX-Shell:
.env aus der Vorlage anlegen, ein APP_SECRET wuerfeln (openssl, sonst
/dev/urandom) und die Rechte auf 600 setzen. Ersetzt wird zeilenweise statt
mit einem sed-Ausdruck, weil base64 Schraegstriche enthaelt. update.sh liest
die Version jetzt selbst aus der package.json.

Dabei fielen zwei Fehler auf, die mein eigener Rat verursacht hat. Die
Anleitung empfiehlt APP_PORT="127.0.0.1:3000", damit die Anwendung nur hinter
dem Reverse Proxy erreichbar ist - die Gesundheitspruefung baute daraus aber
"http://127.0.0.1:127.0.0.1:3000". Und curl ist auf einem Docker-Host nicht
garantiert. Gefragt wird deshalb jetzt der Container selbst ueber seinen
HEALTHCHECK; curl liefert nur noch die Antwort zum Anzeigen, wenn es da ist.

Am Ende nannte das Skript den Sicherungspfad mit Anfuehrungszeichen - so
liess er sich nicht kopieren.

Nachgewiesen: dreimal gegen die laufende Instanz durchgelaufen, jedes Mal
mit Sicherung, Bau, Neustart und gesunder Antwort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 00:26:46 +02:00
co-authored by Claude Opus 5
parent 47245152a2
commit 6808d4d34a
3 changed files with 129 additions and 24 deletions
+12 -5
View File
@@ -13,6 +13,7 @@ Für Installation zum Ausprobieren und die Entwicklungsvarianten siehe
| | | | | |
| --- | --- | | --- | --- |
| Server | Linux mit Docker Engine ≥ 24 und Compose v2 | | Server | Linux mit Docker Engine ≥ 24 und Compose v2 |
| Sonst | `git`, `curl`, eine POSIX-Shell — **kein Node, kein npm** |
| Arbeitsspeicher | 1 GB reichen; beim Bauen kurzzeitig ~2 GB | | Arbeitsspeicher | 1 GB reichen; beim Bauen kurzzeitig ~2 GB |
| Plattenplatz | ~2 GB für Abbilder, dazu die Datenbank | | Plattenplatz | ~2 GB für Abbilder, dazu die Datenbank |
| Domain | Eine (Unter-)Domain, die auf den Server zeigt | | Domain | Eine (Unter-)Domain, die auf den Server zeigt |
@@ -46,14 +47,19 @@ cd /opt/nebula
## 3 · Konfigurieren ## 3 · Konfigurieren
```bash ```bash
node scripts/setup.mjs # legt .env an und würfelt ein APP_SECRET ./scripts/setup.sh # legt .env an, würfelt ein APP_SECRET, setzt Rechte 600
``` ```
Ohne Node auf dem Server: `cp .env.example .env` und das Geheimnis von Hand Das Skript ist reines POSIX-Shell und braucht **kein Node** — auf einem
setzen — reinen Docker-Host ist keins installiert. Es nimmt `openssl`, falls
vorhanden, sonst `/dev/urandom`.
Ganz von Hand geht auch:
```bash ```bash
openssl rand -base64 48 cp .env.example .env
chmod 600 .env
openssl rand -base64 32 # den Wert als APP_SECRET eintragen
``` ```
Dann die `.env` öffnen und mindestens diese vier Werte anpassen: Dann die `.env` öffnen und mindestens diese vier Werte anpassen:
@@ -227,11 +233,12 @@ Was es tut:
| Schritt | | | Schritt | |
| --- | --- | | --- | --- |
| **1 Prüfen** | docker, compose, `.env` da? Ist `APP_SECRET` gesetzt und nicht mehr der Entwicklungswert? | | **1 Prüfen** | docker, compose, `.env` da? Ist `APP_SECRET` gesetzt und nicht mehr der Entwicklungswert? |
| | *(alles ohne Node — das Skript liest die Version selbst aus der `package.json`)* |
| **2 Holen** | `git pull --ff-only`; zeigt die neuen Commits und fragt nach | | **2 Holen** | `git pull --ff-only`; zeigt die neuen Commits und fragt nach |
| **3 Sichern** | `pg_dump` **vor** jeder Schemaänderung | | **3 Sichern** | `pg_dump` **vor** jeder Schemaänderung |
| **4 Bauen** | Erst bauen, dann umschalten — ein gescheiterter Bau lässt die alte Fassung laufen | | **4 Bauen** | Erst bauen, dann umschalten — ein gescheiterter Bau lässt die alte Fassung laufen |
| **5 Starten** | Container neu; die Migrationen laufen im Entrypoint | | **5 Starten** | Container neu; die Migrationen laufen im Entrypoint |
| **6 Prüfen** | Bis zu 60 Sekunden auf `/api/health` warten | | **6 Prüfen** | Bis zu zwei Minuten warten, bis der Container sich gesund meldet |
Scheitert die Gesundheitsprüfung, zeigt das Skript die letzten Logzeilen und Scheitert die Gesundheitsprüfung, zeigt das Skript die letzten Logzeilen und
nennt den Weg zurück. nennt den Weg zurück.
+79
View File
@@ -0,0 +1,79 @@
#!/bin/sh
# ─────────────────────────────────────────────────────────────
# Legt die .env an und wuerfelt ein APP_SECRET.
#
# Dasselbe wie scripts/setup.mjs, aber ohne Node: auf einem reinen
# Docker-Host ist keins installiert, und nur zum Erzeugen einer Textdatei
# eines zu verlangen waere albern. Braucht nichts ausser einer POSIX-Shell.
#
# Aufruf: ./scripts/setup.sh
# ./scripts/setup.sh --force (vorhandene .env ueberschreiben)
# ─────────────────────────────────────────────────────────────
set -eu
root="$(cd "$(dirname "$0")/.." && pwd)"
env_datei="$root/.env"
vorlage="$root/.env.example"
force=0
[ "${1:-}" = "--force" ] && force=1
if [ -t 1 ]; then
gruen=$'\033[32m'; gelb=$'\033[33m'; cyan=$'\033[36m'; dim=$'\033[2m'; aus=$'\033[0m'
else
gruen=""; gelb=""; cyan=""; dim=""; aus=""
fi
[ -f "$vorlage" ] || { echo "${gelb}.env.example nicht gefunden – abgebrochen.${aus}" >&2; exit 1; }
# ── Zufall ───────────────────────────────────────────────────
# openssl ist auf den meisten Systemen da, aber nicht auf allen. /dev/urandom
# gibt es ueberall, wo diese Shell laeuft – und base64 ebenso.
zufall() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 32
else
head -c 32 /dev/urandom | base64
fi | tr -d '\n'
}
if [ -f "$env_datei" ] && [ "$force" -eq 0 ]; then
echo "${gelb}·${aus} .env existiert bereits – sie wird nicht ueberschrieben."
echo " ${dim}Mit --force neu anlegen (die alte wird gesichert).${aus}"
else
if [ -f "$env_datei" ]; then
sicherung="$env_datei.$(date +%Y%m%d%H%M%S).bak"
cp "$env_datei" "$sicherung"
echo "${gelb}·${aus} Alte .env gesichert: $(basename "$sicherung")"
fi
secret="$(zufall)"
# Das Geheimnis kann / und + enthalten; als sed-Ersetzung waeren das
# Sonderzeichen. Deshalb zeilenweise ersetzen statt mit einem Ausdruck.
: > "$env_datei"
while IFS= read -r zeile || [ -n "$zeile" ]; do
case "$zeile" in
APP_SECRET=*) printf 'APP_SECRET="%s"\n' "$secret" >> "$env_datei" ;;
*) printf '%s\n' "$zeile" >> "$env_datei" ;;
esac
done < "$vorlage"
chmod 600 "$env_datei"
echo "${gruen}✓${aus} .env angelegt, APP_SECRET zufaellig erzeugt, Rechte auf 600."
fi
echo
echo "${cyan}Noch anzupassen${aus}"
echo "${dim}─────────────────────────────────────────────${aus}"
echo " APP_URL die Adresse, unter der du Nebula aufrufst"
echo " ${dim}bestimmt die WebAuthn-rpID – passt sie nicht, scheitern Passkeys${aus}"
echo " POSTGRES_PASSWORD die Voreinstellung ist fuer Entwicklung gedacht"
echo " APP_PORT auf einem Server: 127.0.0.1:3000 hinter den Reverse Proxy"
echo
echo "${cyan}Danach${aus}"
echo "${dim}─────────────────────────────────────────────${aus}"
echo " docker compose --profile full up -d --build"
echo " ${dim}Der Container spielt die Migrationen beim Start selbst ein.${aus}"
echo
echo " Ausfuehrlich: DEPLOYMENT.md"
echo
+38 -19
View File
@@ -40,6 +40,13 @@ else
rot=""; gruen=""; gelb=""; fett=""; aus="" rot=""; gruen=""; gelb=""; fett=""; aus=""
fi fi
# Die Version aus der package.json lesen – ohne Node. Auf einem reinen
# Docker-Host ist keins installiert, und dieses Skript soll dort laufen.
paket_version() {
sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$root/package.json" \
| head -n 1
}
schritt() { printf '\n%s▸ %s%s\n' "$fett" "$1" "$aus"; } schritt() { printf '\n%s▸ %s%s\n' "$fett" "$1" "$aus"; }
ok() { printf ' %s✓%s %s\n' "$gruen" "$aus" "$1"; } ok() { printf ' %s✓%s %s\n' "$gruen" "$aus" "$1"; }
warnung() { printf ' %s!%s %s\n' "$gelb" "$aus" "$1"; } warnung() { printf ' %s!%s %s\n' "$gelb" "$aus" "$1"; }
@@ -50,7 +57,7 @@ schritt "Voraussetzungen"
command -v docker >/dev/null || abbruch "docker nicht gefunden." command -v docker >/dev/null || abbruch "docker nicht gefunden."
docker compose version >/dev/null 2>&1 || abbruch "docker compose (v2) nicht gefunden." docker compose version >/dev/null 2>&1 || abbruch "docker compose (v2) nicht gefunden."
[ -f "$root/.env" ] || abbruch "Keine .env in $root. Zuerst 'node scripts/setup.mjs' ausfuehren." [ -f "$root/.env" ] || abbruch "Keine .env in $root. Zuerst './scripts/setup.sh' ausfuehren."
[ -f "$root/docker-compose.yml" ] || abbruch "Kein docker-compose.yml in $root." [ -f "$root/docker-compose.yml" ] || abbruch "Kein docker-compose.yml in $root."
ok "docker, compose und .env vorhanden" ok "docker, compose und .env vorhanden"
@@ -61,7 +68,7 @@ fi
ok "APP_SECRET gesetzt" ok "APP_SECRET gesetzt"
vorher="$(git rev-parse --short HEAD 2>/dev/null || echo "unbekannt")" vorher="$(git rev-parse --short HEAD 2>/dev/null || echo "unbekannt")"
version_vorher="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")" version_vorher="$(paket_version)"
printf ' Aktuell: v%s (%s)\n' "$version_vorher" "$vorher" printf ' Aktuell: v%s (%s)\n' "$version_vorher" "$vorher"
# ── Neuen Stand holen ──────────────────────────────────────── # ── Neuen Stand holen ────────────────────────────────────────
@@ -83,7 +90,7 @@ if [ "$pull" -eq 1 ]; then
fi fi
nachher="$(git rev-parse --short HEAD)" nachher="$(git rev-parse --short HEAD)"
version_nachher="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")" version_nachher="$(paket_version)"
if [ "$vorher" = "$nachher" ]; then if [ "$vorher" = "$nachher" ]; then
warnung "Kein neuer Stand – es wird trotzdem neu gebaut." warnung "Kein neuer Stand – es wird trotzdem neu gebaut."
@@ -111,7 +118,9 @@ docker compose up -d db >/dev/null
sicherung="" sicherung=""
if [ -x "$root/scripts/backup.sh" ]; then if [ -x "$root/scripts/backup.sh" ]; then
ausgabe="$("$root/scripts/backup.sh" 2>&1)" || abbruch "Sicherung fehlgeschlagen:\n$ausgabe" ausgabe="$("$root/scripts/backup.sh" 2>&1)" || abbruch "Sicherung fehlgeschlagen:\n$ausgabe"
sicherung="$(printf '%s' "$ausgabe" | grep -oE '[^ ]*backups/[^ ]*\.sql[^ ]*' | tail -n 1 || true)" # backup.sh setzt den Pfad in Anfuehrungszeichen – die gehoeren nicht in
# den Namen, sonst laesst sich der Vorschlag am Ende nicht kopieren.
sicherung="$(printf '%s' "$ausgabe" | grep -oE '[^ "]*backups/[^ "]*\.sql[^ "]*' | tail -n 1 || true)"
ok "Gesichert${sicherung:+: $sicherung}" ok "Gesichert${sicherung:+: $sicherung}"
else else
warnung "scripts/backup.sh fehlt – es wird ohne Sicherung fortgefahren." warnung "scripts/backup.sh fehlt – es wird ohne Sicherung fortgefahren."
@@ -132,16 +141,17 @@ ok "Container gestartet"
# ── Gesundheitspruefung ────────────────────────────────────── # ── Gesundheitspruefung ──────────────────────────────────────
schritt "Gesundheitspruefung" schritt "Gesundheitspruefung"
port="$(sed -n 's/^[[:space:]]*APP_PORT[[:space:]]*=[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' "$root/.env" | tail -n 1)" # Gefragt wird der Container selbst, nicht der Host. Das Abbild bringt einen
port="${port:-3000}" # HEALTHCHECK mit, und so ist es einerlei, ob APP_PORT nur eine Nummer ist
# oder wie auf einem Server "127.0.0.1:3000" – und curl braucht es auch nicht.
gesund=0 gesund=0
for versuch in $(seq 1 30); do for _ in $(seq 1 40); do
if curl -fsS "http://127.0.0.1:${port}/api/health" >/dev/null 2>&1; then zustand="$(docker inspect --format '{{.State.Health.Status}}' nebula-app 2>/dev/null || echo "weg")"
gesund=1 case "$zustand" in
break healthy) gesund=1; break ;;
fi unhealthy) break ;;
sleep 2 esac
sleep 3
done done
if [ "$gesund" -ne 1 ]; then if [ "$gesund" -ne 1 ]; then
@@ -150,14 +160,23 @@ if [ "$gesund" -ne 1 ]; then
printf '\n%sZurueck zur vorigen Fassung:%s\n' "$fett" "$aus" printf '\n%sZurueck zur vorigen Fassung:%s\n' "$fett" "$aus"
printf ' git checkout %s && ./scripts/update.sh --no-pull --yes\n' "$vorher" printf ' git checkout %s && ./scripts/update.sh --no-pull --yes\n' "$vorher"
[ -n "$sicherung" ] && printf ' Datenbank notfalls: ./scripts/restore.sh %s\n' "$sicherung" [ -n "$sicherung" ] && printf ' Datenbank notfalls: ./scripts/restore.sh %s\n' "$sicherung"
abbruch "Die Anwendung antwortet nach 60 Sekunden nicht gesund." abbruch "Der Container meldet sich nach zwei Minuten nicht gesund (Zustand: ${zustand})."
fi
ok "Container meldet sich gesund"
# Die Anzeige der Adresse: aus APP_PORT kann "3000" oder "127.0.0.1:3000"
# kommen – fuer die Ausgabe zaehlt nur, was hinter dem letzten Doppelpunkt steht.
port_roh="$(sed -n 's/^[[:space:]]*APP_PORT[[:space:]]*=[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' "$root/.env" | tail -n 1)"
port_roh="${port_roh:-3000}"
port="${port_roh##*:}"
if command -v curl >/dev/null 2>&1; then
antwort_json="$(curl -fsS "http://127.0.0.1:${port}/api/health" 2>/dev/null || true)"
[ -n "$antwort_json" ] && ok "Antwort: $antwort_json"
fi fi
antwort_json="$(curl -fsS "http://127.0.0.1:${port}/api/health")"
ok "Gesund: $antwort_json"
# ── Fertig ─────────────────────────────────────────────────── # ── Fertig ───────────────────────────────────────────────────
version_jetzt="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")" version_jetzt="$(paket_version)"
printf '\n%s%s✓ Nebula v%s laeuft auf Port %s%s\n' "$gruen" "$fett" "$version_jetzt" "$port" "$aus" printf '\n%s%s✓ Nebula v%s laeuft auf %s%s\n' "$gruen" "$fett" "$version_jetzt" "$port_roh" "$aus"
[ -n "$sicherung" ] && printf ' Sicherung vor dem Update: %s\n' "$sicherung" [ -n "$sicherung" ] && printf ' Sicherung vor dem Update: %s\n' "$sicherung"
printf ' Log mitlesen: docker compose --profile full logs -f app\n\n' printf ' Log mitlesen: docker compose --profile full logs -f app\n\n'