#!/usr/bin/env bash # # Werkbaum — Backend auf die stabile Instanz bringen (D77). # # Baut das Fat-Jar, legt es neben ein systemd-User-Unit ins Home des Servers # und startet den Dienst neu. Der Dienst lauscht nur auf 127.0.0.1; von außen # kommt man über die Proxy-Regel in `scripts/prod.htaccess`, die # `scripts/deploy-prod.sh` mitspiegelt (D76-Nachtrag 2). # # Verwendung: # scripts/deploy-backend.sh [-y] [--no-build] [--no-restart] [ssh-ziel] # # -y ohne Rückfrage spiegeln und neu starten # --no-build vorhandenes Jar nehmen (z. B. Wiederholung eines Deploys) # --no-restart nur hochladen, Dienst nicht anfassen # # Konfiguration aus der git-ignorierten `.env` (Vorlage: .env.example): # BACKEND_SSH user@host — Pflicht (oder als Argument) # BACKEND_DIR Zielverzeichnis, Vorgabe opt/werkbaum — relativ zum Home # des Servers (oder absolut, wenn es mit / beginnt) # BACKEND_JDK_DIR JDK, Vorgabe opt/jdk21 (scripts/install-jdk.sh legt es an) # BACKEND_PORT Vorgabe 9080 — dieselbe Zahl setzt deploy-prod.sh in die # .htaccess ein; sie ist die eine Vereinbarung zwischen # Apache und Dienst # BACKEND_XMX Vorgabe 192m — gemessen leben nach einem GC ~45 MB; # siehe die Messreihe in der Unit-Vorlage # # Das Master-Passwort steht NICHT hier und nicht im Repository. Es gehört in # `/env` auf dem Server (Modus 600); dieses Skript legt die Datei # beim ersten Mal mit leerem Hash an und sagt, was zu tun ist. Ohne Hash bleibt # die Dokumentenliste gesperrt — das ist Absicht (D76-Nachtrag 6). # # Siehe docs/DECISIONS.md D77 und backend/README.md. set -euo pipefail YES=0 BUILD=1 RESTART=1 SSH_TARGET="" for arg in "$@"; do case "$arg" in -y|--yes) YES=1 ;; --no-build) BUILD=0 ;; --no-restart) RESTART=0 ;; -h|--help) awk 'NR>2 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$0" exit 0 ;; -*) echo "Unbekannte Option: $arg" >&2; exit 2 ;; *) if [ -n "$SSH_TARGET" ]; then echo "Zu viele Argumente." >&2; exit 2; fi SSH_TARGET="$arg" ;; esac done ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" # shellcheck source=/dev/null . "$ROOT/scripts/lib-env.sh" [ -n "$SSH_TARGET" ] || SSH_TARGET="$(env_value BACKEND_SSH)" if [ -z "$SSH_TARGET" ]; then echo "Usage: $0 [-y] (oder BACKEND_SSH in .env)" >&2 exit 2 fi DIR="$(env_value BACKEND_DIR)"; DIR="${DIR:-opt/werkbaum}" JDK_DIR="$(env_value BACKEND_JDK_DIR)"; JDK_DIR="${JDK_DIR:-opt/jdk21}" PORT="$(env_value BACKEND_PORT)"; PORT="${PORT:-9080}" XMX="$(env_value BACKEND_XMX)"; XMX="${XMX:-192m}" # Zweimal derselbe Pfad in zwei Schreibweisen: systemd kennt `%h`, die Shell # kennt `$HOME` — und systemd expandiert `$HOME` nicht (siehe Unit-Vorlage). case "$DIR" in /*) DIR_UNIT="$DIR"; DIR_SH="$DIR" ;; *) DIR_UNIT="%h/$DIR"; DIR_SH="\$HOME/$DIR" ;; esac case "$JDK_DIR" in /*) JAVA_UNIT="$JDK_DIR/bin/java" ;; *) JAVA_UNIT="%h/$JDK_DIR/bin/java" ;; esac echo "==> Ziel ${SSH_TARGET}:~/${DIR#/}, Port ${PORT}, -Xmx${XMX}" # ---- 1) Bauen ---- JAR="" if [ "$BUILD" -eq 1 ]; then echo "==> ./gradlew bootJar (mit Tests)" (cd backend && ./gradlew --quiet check bootJar) else echo "==> Build übersprungen (--no-build)" fi JAR="$(ls -1t backend/build/libs/*.jar 2>/dev/null | grep -v -- '-plain\.jar$' | head -1 || true)" if [ -z "$JAR" ]; then echo "Kein Fat-Jar in backend/build/libs — ohne --no-build versuchen." >&2 exit 1 fi echo " ${JAR} ($(du -h "$JAR" | cut -f1))" # ---- 2) Unit-Datei aus der Vorlage ---- # Die Platzhalter werden hier ersetzt, nicht auf dem Server: So steht im # Repository die Vorlage und am Ziel genau eine fertige Datei — nichts, was # beide Seiten unterschiedlich interpretieren könnten. STAGE="$(mktemp -d)" trap 'rm -rf "$STAGE"' EXIT sed -e "s#__JAVA__#${JAVA_UNIT}#g" \ -e "s#__DIR__#${DIR_UNIT}#g" \ -e "s#__XMX__#${XMX}#g" \ -e "s#__PORT__#${PORT}#g" \ scripts/werkbaum-backend.service > "$STAGE/werkbaum-backend.service" cp "$JAR" "$STAGE/werkbaum-backend.jar" if [ "$YES" -ne 1 ]; then echo "==> Es werden übertragen:" echo " werkbaum-backend.jar -> ${DIR}/" echo " werkbaum-backend.service -> ~/.config/systemd/user/" [ "$RESTART" -eq 1 ] && echo " und der Dienst neu gestartet." printf '==> Weiter? [y/N] ' read -r ANS case "$ANS" in y|Y|j|J) : ;; *) echo "Abgebrochen."; exit 1 ;; esac fi # ---- 3) Übertragen und einrichten ---- # Bewusst OHNE --delete: Im Zielverzeichnis liegen die Datenbank (`data/`), das # Log und die Datei mit dem Master-Passwort. Ein Deploy tauscht das Jar, er # räumt nicht auf. echo "==> Übertragen" ssh "$SSH_TARGET" "mkdir -p \"$DIR_SH\" \"\$HOME/.config/systemd/user\"" rsync -az --chmod=F644 "$STAGE/werkbaum-backend.jar" "$SSH_TARGET:$DIR_SH/werkbaum-backend.jar" rsync -az --chmod=F644 "$STAGE/werkbaum-backend.service" \ "$SSH_TARGET:\$HOME/.config/systemd/user/werkbaum-backend.service" ssh "$SSH_TARGET" DIR="$DIR_SH" PORT="$PORT" RESTART="$RESTART" 'bash -s' <<'REMOTE' set -euo pipefail DIR="$(eval echo "$DIR")" # Ohne XDG_RUNTIME_DIR findet `systemctl --user` seinen Manager nicht — über # eine nicht-interaktive SSH-Sitzung ist die Variable oft nicht gesetzt. Das # ist die klassische Falle bei User-Units aus einem Skript heraus. export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" # Umgebungsdatei: beim ersten Mal anlegen, danach nie anfassen — dort steht das # Master-Passwort, und ein Deploy darf es weder überschreiben noch lesen. if [ ! -f "$DIR/env" ]; then cat > "$DIR/env" <<'ENV' # Werkbaum-Backend — Umgebung des Dienstes. Modus 600, nie im Repository. # # Hash MIT Verfahrens-Praefix, z. B.: # WERKBAUM_MASTER_PASSWORD_HASH={bcrypt}$(htpasswd -bnBC 12 "" geheim | tr -d ':\n') # Solange er leer ist, bleibt GET /api/v1/documents gesperrt (D76-Nachtrag 6). WERKBAUM_MASTER_PASSWORD_HASH= ENV chmod 600 "$DIR/env" echo " ! $DIR/env angelegt — Master-Passwort-Hash dort eintragen," echo " sonst bleibt die Dokumentenliste gesperrt." fi chmod 600 "$DIR/env" if ! loginctl show-user "$(id -un)" -p Linger 2>/dev/null | grep -q 'Linger=yes'; then echo " ! Linger ist nicht gesetzt — der Dienst endet mit der Sitzung." echo " Abhilfe: loginctl enable-linger $(id -un)" fi systemctl --user daemon-reload if [ "$RESTART" = "1" ]; then systemctl --user enable --now werkbaum-backend.service >/dev/null 2>&1 || true systemctl --user restart werkbaum-backend.service echo " Dienst neu gestartet." else echo " Dienst nicht angefasst (--no-restart)." fi REMOTE # ---- 4) Nachsehen, ob er wirklich antwortet ---- if [ "$RESTART" -eq 1 ]; then echo "==> Warten, bis der Dienst antwortet" # Es gibt (noch) keinen Health-Endpunkt — der steht als eigener Knoten im # Plan (`#be.scaffold.ci`). Bis dahin ist die ehrlichste Lebendprobe eine # Anfrage nach einem Dokument, das es nicht gibt: 404 heißt, die Anwendung # ist oben und beantwortet Anfragen. PROBE="http://127.0.0.1:${PORT}/api/v1/documents/00000000-0000-0000-0000-000000000000" if ssh "$SSH_TARGET" "for i in \$(seq 1 45); do code=\$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 '$PROBE' || true) if [ \"\$code\" = '404' ]; then echo \" oben nach \${i}s (HTTP 404 wie erwartet)\"; exit 0; fi sleep 1 done echo ' ! antwortet nicht.'; exit 1"; then : else echo "==> Letzte Zeilen des Logs:" >&2 ssh "$SSH_TARGET" "export XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/\$(id -u)}; tail -30 \"$DIR_SH/backend.log\" 2>/dev/null || systemctl --user status werkbaum-backend --no-pager -l | tail -30" >&2 exit 1 fi fi echo "==> Fertig." echo " Log: ssh $SSH_TARGET tail -f $DIR_SH/backend.log" echo " Zustand: ssh $SSH_TARGET systemctl --user status werkbaum-backend"