Files
anouma/setup.sh
T
maroandClaude Sonnet 5 50c39a70e0 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>
2026-08-25 21:50:28 +02:00

339 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 ""