diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index e3b01b8..1d66048 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -13,6 +13,7 @@ Für Installation zum Ausprobieren und die Entwicklungsvarianten siehe | | | | --- | --- | | 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 | | Plattenplatz | ~2 GB für Abbilder, dazu die Datenbank | | Domain | Eine (Unter-)Domain, die auf den Server zeigt | @@ -46,14 +47,19 @@ cd /opt/nebula ## 3 · Konfigurieren ```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 -setzen — +Das Skript ist reines POSIX-Shell und braucht **kein Node** — auf einem +reinen Docker-Host ist keins installiert. Es nimmt `openssl`, falls +vorhanden, sonst `/dev/urandom`. + +Ganz von Hand geht auch: ```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: @@ -227,11 +233,12 @@ Was es tut: | Schritt | | | --- | --- | | **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 | | **3 Sichern** | `pg_dump` **vor** jeder Schemaänderung | | **4 Bauen** | Erst bauen, dann umschalten — ein gescheiterter Bau lässt die alte Fassung laufen | | **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 nennt den Weg zurück. diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..30d82e2 --- /dev/null +++ b/scripts/setup.sh @@ -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 diff --git a/scripts/update.sh b/scripts/update.sh index 9599f9d..ce3f405 100644 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -40,6 +40,13 @@ else rot=""; gruen=""; gelb=""; fett=""; aus="" 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"; } ok() { printf ' %s✓%s %s\n' "$gruen" "$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." 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." ok "docker, compose und .env vorhanden" @@ -61,7 +68,7 @@ fi ok "APP_SECRET gesetzt" 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" # ── Neuen Stand holen ──────────────────────────────────────── @@ -83,7 +90,7 @@ if [ "$pull" -eq 1 ]; then fi 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 warnung "Kein neuer Stand – es wird trotzdem neu gebaut." @@ -111,7 +118,9 @@ docker compose up -d db >/dev/null sicherung="" if [ -x "$root/scripts/backup.sh" ]; then 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}" else warnung "scripts/backup.sh fehlt – es wird ohne Sicherung fortgefahren." @@ -132,16 +141,17 @@ ok "Container gestartet" # ── Gesundheitspruefung ────────────────────────────────────── schritt "Gesundheitspruefung" -port="$(sed -n 's/^[[:space:]]*APP_PORT[[:space:]]*=[[:space:]]*"\{0,1\}\([^"#]*\)"\{0,1\}[[:space:]]*$/\1/p' "$root/.env" | tail -n 1)" -port="${port:-3000}" - +# Gefragt wird der Container selbst, nicht der Host. Das Abbild bringt einen +# 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 -for versuch in $(seq 1 30); do - if curl -fsS "http://127.0.0.1:${port}/api/health" >/dev/null 2>&1; then - gesund=1 - break - fi - sleep 2 +for _ in $(seq 1 40); do + zustand="$(docker inspect --format '{{.State.Health.Status}}' nebula-app 2>/dev/null || echo "weg")" + case "$zustand" in + healthy) gesund=1; break ;; + unhealthy) break ;; + esac + sleep 3 done 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 ' git checkout %s && ./scripts/update.sh --no-pull --yes\n' "$vorher" [ -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 -antwort_json="$(curl -fsS "http://127.0.0.1:${port}/api/health")" -ok "Gesund: $antwort_json" - # ── Fertig ─────────────────────────────────────────────────── -version_jetzt="$(node -p "require('./package.json').version" 2>/dev/null || echo "?")" -printf '\n%s%s✓ Nebula v%s laeuft auf Port %s%s\n' "$gruen" "$fett" "$version_jetzt" "$port" "$aus" +version_jetzt="$(paket_version)" +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" printf ' Log mitlesen: docker compose --profile full logs -f app\n\n'