Add full Docker deployment: setup.sh, update.sh, healthcheck, TURN support

- setup.sh: interactive/non-interactive one-shot installer (build, DB
  healthcheck, migrate, seed, start), idempotent secret generation, NPM
  reverse-proxy network auto-detection and optional join, optional
  AUTO_UPDATE cron install.
- update.sh: release-tag-gated updates only (never bare main), DB backup
  with retention before every update, lock file against concurrent runs,
  automatic code rollback on failed post-update healthcheck.
- Dockerfile: multi-stage build, non-root user, built-in HEALTHCHECK against
  the new /api/health route, wholesale COPY so new source dirs (e.g.
  scripts/) never silently go missing at runtime.
- docker-compose.yml: internal anouma-network (configurable), named volume
  for Postgres, app depends_on postgres healthy, no unnecessary published
  ports; docker-compose.override.yml.example documents joining an existing
  NPM network without ever touching NPM itself.
- Fix host-detection: isHost was Boolean(user), wrongly granting host
  privileges to logged-in customers; now checks user.collection === "users".
- Wire configurable STUN/TURN servers through to the WebRTC client
  (lib/meeting/iceServers.ts) so a TURN server can be added later via env
  vars only, no code changes.
- DEPLOYMENT.md, updated README.md and .env.example documenting the whole
  flow: NPM integration, env vars, WebRTC, updates, backups, rollback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 21:50:28 +02:00
co-authored by Claude Sonnet 5
parent d3d53e68a9
commit 50c39a70e0
16 changed files with 886 additions and 68 deletions
Executable
+338
View File
@@ -0,0 +1,338 @@
#!/usr/bin/env bash
# ANOUMA — automated setup.
#
# Fresh install:
# git clone https://git.maro.run/maro/anouma.git && cd anouma && ./setup.sh
#
# Safe to re-run on an existing installation: never overwrites secrets that
# already exist in .env, never deletes volumes/networks/containers it
# doesn't own, and never touches Nginx Proxy Manager (or any other existing
# container) beyond optionally joining its Docker network.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
COLOR_INFO="\033[36m"; COLOR_WARN="\033[33m"; COLOR_ERROR="\033[31m"; COLOR_OK="\033[32m"; COLOR_RESET="\033[0m"
log_info() { printf "${COLOR_INFO}[INFO]${COLOR_RESET} %s\n" "$1"; }
log_warn() { printf "${COLOR_WARN}[WARN]${COLOR_RESET} %s\n" "$1"; }
log_error() { printf "${COLOR_ERROR}[ERROR]${COLOR_RESET} %s\n" "$1" >&2; }
log_success() { printf "${COLOR_OK}[SUCCESS]${COLOR_RESET} %s\n" "$1"; }
fail() {
log_error "$1"
[ -n "${2:-}" ] && log_error "$2"
exit 1
}
# ---------------------------------------------------------------------------
# Args
# ---------------------------------------------------------------------------
NON_INTERACTIVE=false
SKIP_SEED=false
for arg in "$@"; do
case "$arg" in
--non-interactive) NON_INTERACTIVE=true ;;
--skip-seed) SKIP_SEED=true ;;
--help|-h)
echo "Usage: ./setup.sh [--non-interactive] [--skip-seed]"
exit 0
;;
esac
done
ask() {
# ask "Prompt" "default" -> echoes the answer
local prompt="$1" default="$2" answer
if [ "$NON_INTERACTIVE" = true ] || [ ! -t 0 ]; then
echo "$default"
return
fi
read -r -p "$prompt [$default]: " answer || true
echo "${answer:-$default}"
}
ask_yn() {
# ask_yn "Prompt" "Y|N" -> echoes true/false
local prompt="$1" default="$2" answer
if [ "$NON_INTERACTIVE" = true ] || [ ! -t 0 ]; then
[ "$default" = "Y" ] && echo true || echo false
return
fi
local hint="y/N"; [ "$default" = "Y" ] && hint="Y/n"
read -r -p "$prompt [$hint]: " answer || true
answer="${answer:-$default}"
case "$answer" in
y|Y|yes|Yes|YES) echo true ;;
*) echo false ;;
esac
}
echo ""
echo "ANOUMA Setup"
echo "============"
echo ""
# ---------------------------------------------------------------------------
# 14: detect tooling
# ---------------------------------------------------------------------------
command -v docker >/dev/null 2>&1 || fail "Docker wurde nicht gefunden." "Installiere Docker: https://docs.docker.com/engine/install/"
log_success "Docker erkannt: $(docker --version)"
docker info >/dev/null 2>&1 || fail "Der Docker-Daemon läuft nicht (oder fehlende Berechtigung)." "Starte Docker bzw. führe dieses Skript mit einem Nutzer aus, der Docker verwenden darf."
COMPOSE=""
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"
fi
[ -n "$COMPOSE" ] || fail "Docker Compose wurde nicht gefunden." "Installiere das Compose-Plugin: https://docs.docker.com/compose/install/"
log_success "Docker Compose erkannt: $($COMPOSE version | head -n1)"
if command -v git >/dev/null 2>&1; then
log_success "Git erkannt: $(git --version)"
else
log_warn "Git wurde nicht gefunden — für update.sh (Release-Erkennung) empfohlen, aber für den Betrieb selbst nicht zwingend nötig."
fi
# Node/npm are NOT required on the host — everything runs inside the app
# container. This is purely informational.
if command -v node >/dev/null 2>&1; then
log_info "Node.js auf dem Host erkannt: $(node --version) (wird nicht benötigt — ANOUMA läuft in Docker)"
else
log_info "Node.js nicht auf dem Host installiert — kein Problem, ANOUMA läuft vollständig in Docker."
fi
# ---------------------------------------------------------------------------
# 56: detect existing Docker networks, look for a reverse proxy (e.g. NPM)
# ---------------------------------------------------------------------------
log_info "Prüfe vorhandene Docker-Netzwerke …"
EXISTING_NETWORKS="$(docker network ls --format '{{.Name}}' 2>/dev/null || true)"
detect_npm_network() {
# 1) Look for a running container that looks like Nginx Proxy Manager and
# read the network it's actually attached to.
local npm_container
npm_container="$(docker ps --format '{{.Names}}\t{{.Image}}' 2>/dev/null | grep -iE 'nginx-proxy-manager|jc21/nginx-proxy-manager' | head -n1 | cut -f1 || true)"
if [ -n "$npm_container" ]; then
docker inspect "$npm_container" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' 2>/dev/null | grep -v '^bridge$' | head -n1
return
fi
# 2) Fall back to a name heuristic among existing networks.
echo "$EXISTING_NETWORKS" | grep -iE 'nginx-proxy-manager|^npm' | head -n1 || true
}
DETECTED_NPM_NETWORK="$(detect_npm_network || true)"
NPM_DETECTED=false
if [ -n "$DETECTED_NPM_NETWORK" ]; then
NPM_DETECTED=true
log_success "Nginx Proxy Manager erkannt (Netzwerk: $DETECTED_NPM_NETWORK)"
else
log_info "Nginx Proxy Manager wurde nicht erkannt — ANOUMA läuft eigenständig, das ist kein Fehler."
fi
# ---------------------------------------------------------------------------
# 710: .env — create if missing, fill in only missing keys otherwise,
# generate secrets, never overwrite existing ones
# ---------------------------------------------------------------------------
gen_secret() { openssl rand -hex 32; }
FRESH_ENV=false
if [ ! -f .env ]; then
FRESH_ENV=true
cp .env.example .env
log_info ".env aus .env.example erstellt."
fi
env_get() { grep -E "^$1=" .env 2>/dev/null | tail -n1 | cut -d'=' -f2- || true; }
env_set() {
# env_set KEY VALUE — replaces an existing (possibly empty) line, or
# appends the key if it's missing entirely. Never touches other keys.
local key="$1" value="$2"
if grep -qE "^$key=" .env; then
local tmp; tmp="$(mktemp)"
awk -v k="$key" -v v="$value" 'BEGIN{FS=OFS="="} $1==k{$0=k "=" v} {print}' .env > "$tmp" && mv "$tmp" .env
else
printf '%s=%s\n' "$key" "$value" >> .env
fi
}
# Only fills the key in if it is currently missing or blank — this is what
# makes secret generation safe to re-run (existing secrets survive updates).
env_ensure() {
local key="$1" value="$2"
local current; current="$(env_get "$key")"
if [ -z "$current" ]; then
env_set "$key" "$value"
return 0
fi
return 1
}
if [ "$FRESH_ENV" = true ]; then
log_info "Domain für diese Installation (z. B. https://anouma.org):"
APP_URL="$(ask "Domain" "http://localhost:3000")"
env_set NEXT_PUBLIC_SERVER_URL "$APP_URL"
fi
env_ensure PAYLOAD_SECRET "$(gen_secret)" && log_info "PAYLOAD_SECRET generiert." || log_info "PAYLOAD_SECRET bereits vorhanden — unverändert."
env_ensure MEETING_SESSION_SECRET "$(gen_secret)" && log_info "MEETING_SESSION_SECRET generiert." || log_info "MEETING_SESSION_SECRET bereits vorhanden — unverändert."
env_ensure CRON_SECRET "$(gen_secret)" && log_info "CRON_SECRET generiert." || log_info "CRON_SECRET bereits vorhanden — unverändert."
if env_ensure POSTGRES_PASSWORD "$(gen_secret)"; then
log_info "POSTGRES_PASSWORD generiert."
# Keep DATABASE_URI in sync with the freshly generated password so the
# app can actually connect — only rewritten when we just generated the
# password ourselves, never when POSTGRES_PASSWORD already existed.
PG_USER="$(env_get POSTGRES_USER)"; PG_USER="${PG_USER:-postgres}"
PG_DB="$(env_get POSTGRES_DB)"; PG_DB="${PG_DB:-anouma}"
PG_PASSWORD="$(env_get POSTGRES_PASSWORD)"
env_set DATABASE_URI "postgresql://${PG_USER}:${PG_PASSWORD}@postgres:5432/${PG_DB}"
else
log_info "POSTGRES_PASSWORD bereits vorhanden — unverändert."
fi
if [ "$FRESH_ENV" = true ]; then
echo ""
SETUP_SMTP="$(ask_yn "SMTP-Zugangsdaten jetzt eintragen? (sonst später manuell in .env)" "N")"
if [ "$SETUP_SMTP" = true ]; then
env_set SMTP_HOST "$(ask "SMTP-Host" "")"
env_set SMTP_PORT "$(ask "SMTP-Port" "587")"
env_set SMTP_USER "$(ask "SMTP-Benutzer" "")"
read -r -s -p "SMTP-Passwort: " smtp_pw || true; echo ""
env_set SMTP_PASSWORD "$smtp_pw"
fi
if [ "$NPM_DETECTED" = true ]; then
USE_NPM="$(ask_yn "Nginx Proxy Manager verwenden (Netzwerk beitreten)?" "Y")"
if [ "$USE_NPM" = true ]; then
NPM_NETWORK_NAME="$(ask "NPM-Docker-Netzwerk" "$DETECTED_NPM_NETWORK")"
env_set NPM_NETWORK "$NPM_NETWORK_NAME"
fi
fi
AUTO_UPDATE_ANSWER="$(ask_yn "Automatische Updates aktivieren?" "N")"
env_set AUTO_UPDATE "$AUTO_UPDATE_ANSWER"
fi
chmod 600 .env || true
log_success ".env ist bereit."
# ---------------------------------------------------------------------------
# Reverse-proxy network override — generated only when configured/detected,
# never required. See docker-compose.override.yml.example for the manual
# equivalent.
# ---------------------------------------------------------------------------
NPM_NETWORK_VALUE="$(env_get NPM_NETWORK)"
if [ -n "$NPM_NETWORK_VALUE" ]; then
if ! echo "$EXISTING_NETWORKS" | grep -qx "$NPM_NETWORK_VALUE"; then
log_warn "NPM_NETWORK=$NPM_NETWORK_VALUE ist in .env gesetzt, aber kein Docker-Netzwerk mit diesem Namen existiert — das Override wird trotzdem geschrieben, `docker compose up` wird aber fehlschlagen, bis das Netzwerk existiert."
fi
sed "s/\${NPM_NETWORK}/$NPM_NETWORK_VALUE/g" docker-compose.override.yml.example > docker-compose.override.yml
log_success "docker-compose.override.yml für Reverse-Proxy-Netzwerk '$NPM_NETWORK_VALUE' erstellt."
elif [ -f docker-compose.override.yml ]; then
log_info "NPM_NETWORK ist leer, aber docker-compose.override.yml existiert bereits — wird unverändert gelassen (nicht automatisch gelöscht)."
fi
# ---------------------------------------------------------------------------
# 911: build, start database, wait for healthy, migrate
# ---------------------------------------------------------------------------
# Waits for a compose service's container healthcheck to report "healthy".
# Uses `docker inspect` on the container ID (via `compose ps -q`) rather than
# `compose ps --format`, since Go-template support in `ps --format` varies
# across Compose versions while `docker inspect` is stable everywhere.
wait_healthy() {
local service="$1" timeout_iterations="$2" id status
for _ in $(seq 1 "$timeout_iterations"); do
id="$($COMPOSE ps -q "$service" 2>/dev/null || true)"
if [ -n "$id" ]; then
status="$(docker inspect --format '{{.State.Health.Status}}' "$id" 2>/dev/null || true)"
[ "$status" = "healthy" ] && return 0
fi
sleep 2
done
return 1
}
log_info "Baue Docker-Images (das kann beim ersten Mal einige Minuten dauern) …"
$COMPOSE build
log_info "Starte Datenbank …"
$COMPOSE up -d postgres
log_info "Warte auf Datenbank-Healthcheck …"
DB_READY=false
wait_healthy postgres 60 && DB_READY=true
[ "$DB_READY" = true ] || fail "Datenbank wurde nicht rechtzeitig healthy." "Prüfe die Logs: $COMPOSE logs postgres"
log_success "Datenbank ist bereit."
log_info "Führe Datenbank-Migrationen aus …"
$COMPOSE run --rm app npm run migrate || fail "Migration fehlgeschlagen." "Prüfe die Ausgabe oben. Die Datenbank wurde nicht verändert, wenn die Migration atomar fehlgeschlagen ist."
log_success "Migrationen angewendet."
# ---------------------------------------------------------------------------
# 13: seed
# ---------------------------------------------------------------------------
if [ "$SKIP_SEED" = false ]; then
DO_SEED="$(ask_yn "Vorhandene ANOUMA-Inhalte (Angebote, Seitentexte) jetzt einspielen?" "Y")"
if [ "$DO_SEED" = true ]; then
log_info "Spiele Seed-Daten ein …"
$COMPOSE run --rm app npm run seed || log_warn "Seed ist fehlgeschlagen — die Installation läuft trotzdem weiter, Inhalte können später im Admin-Bereich gepflegt werden."
fi
fi
# ---------------------------------------------------------------------------
# 1415: start app, healthcheck
# ---------------------------------------------------------------------------
log_info "Starte Anwendung …"
$COMPOSE up -d app
log_info "Warte auf Healthcheck der Anwendung …"
APP_READY=false
wait_healthy app 60 && APP_READY=true
# ---------------------------------------------------------------------------
# AUTO_UPDATE cron
# ---------------------------------------------------------------------------
if [ "$(env_get AUTO_UPDATE)" = "true" ] && command -v crontab >/dev/null 2>&1; then
CRON_MARKER="# anouma-auto-update"
if ! (crontab -l 2>/dev/null | grep -qF "$CRON_MARKER"); then
( crontab -l 2>/dev/null; echo "*/30 * * * * cd $(pwd) && ./update.sh --auto >> $(pwd)/update.log 2>&1 $CRON_MARKER" ) | crontab -
log_success "Cronjob für automatische Updates eingerichtet (alle 30 Minuten)."
else
log_info "Cronjob für automatische Updates ist bereits eingerichtet."
fi
fi
echo "$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" > .installed-version 2>/dev/null || true
# ---------------------------------------------------------------------------
# 16: result
# ---------------------------------------------------------------------------
echo ""
echo "ANOUMA installation complete"
echo ""
echo "Version:"
echo " $(cat .installed-version 2>/dev/null || echo unknown)"
echo ""
echo "Application:"
echo " $(env_get NEXT_PUBLIC_SERVER_URL)"
echo ""
echo "Docker:"
echo " ✓ Running"
echo ""
echo "Database:"
if [ "$DB_READY" = true ]; then echo " ✓ Healthy"; else echo " ✗ Not healthy — check: $COMPOSE logs postgres"; fi
echo ""
echo "Application healthcheck:"
if [ "$APP_READY" = true ]; then echo " ✓ Running"; else echo " ✗ Not healthy yet — check: $COMPOSE logs app"; fi
echo ""
echo "Nginx Proxy Manager:"
if [ "$NPM_DETECTED" = true ]; then echo " ✓ Detected ($DETECTED_NPM_NETWORK)"; else echo " - Not detected"; fi
echo ""
echo "Next steps:"
echo " 1. Visit ${NPM_DETECTED:+your configured domain, or }http://localhost:${APP_PORT:-3000}/admin to create the first admin account."
echo " 2. See DEPLOYMENT.md for reverse-proxy setup, backups, updates and troubleshooting."
echo ""