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
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# ANOUMA — safe, release-based update.
#
# ./update.sh interactive update to the latest release tag
# ./update.sh --auto used by the AUTO_UPDATE cron job — silently
# does nothing if already on the latest release
# ./update.sh vX.Y.Z update to a specific tag
#
# Never deletes volumes. Never force-deploys an untagged commit from main —
# only real release tags (vX.Y.Z) are deployed. Backs up the database before
# touching anything, and rolls the *code* back (not the database schema —
# migrations should be forward-compatible, see DEPLOYMENT.md) if the
# post-update healthcheck fails.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
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"; exit 1; }
command -v git >/dev/null 2>&1 || fail "Git wird für release-basierte Updates benötigt."
command -v docker >/dev/null 2>&1 || fail "Docker wurde nicht gefunden."
COMPOSE="docker compose"; docker compose version >/dev/null 2>&1 || COMPOSE="docker-compose"
AUTO_MODE=false
TARGET_VERSION=""
for arg in "$@"; do
case "$arg" in
--auto) AUTO_MODE=true ;;
v*) TARGET_VERSION="$arg" ;;
esac
done
# ---------------------------------------------------------------------------
# Update lock — prevents two updates (e.g. a manual one and the cron job)
# from running at the same time.
# ---------------------------------------------------------------------------
LOCK_FILE="$(pwd)/.update.lock"
if [ -f "$LOCK_FILE" ] && kill -0 "$(cat "$LOCK_FILE")" 2>/dev/null; then
log_warn "Update already running."
exit 0
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
[ -f .env ] || fail ".env nicht gefunden — bitte zuerst ./setup.sh ausführen."
[ -d .git ] || fail "Kein Git-Repository — Updates funktionieren nur in einem Checkout von https://git.maro.run/maro/anouma.git."
if [ -n "$(git status --porcelain)" ]; then
fail "Das Arbeitsverzeichnis hat uncommittete Änderungen — Update abgebrochen, um nichts zu überschreiben."
fi
CURRENT_REF="$(git describe --tags --exact-match 2>/dev/null || git rev-parse --short HEAD)"
log_info "Aktuelle Version: $CURRENT_REF"
log_info "Prüfe auf neue Releases …"
git fetch --tags --quiet origin
if [ -n "$TARGET_VERSION" ]; then
NEW_VERSION="$TARGET_VERSION"
else
# Highest vX.Y.Z tag, sorted as real version numbers (not alphabetically).
NEW_VERSION="$(git tag -l 'v*' | sort -t. -k1.2,1n -k2,2n -k3,3n | tail -n1)"
fi
if [ -z "$NEW_VERSION" ]; then
log_info "Keine Release-Tags im Repository gefunden — nichts zu deployen (main wird bewusst nicht automatisch deployt)."
exit 0
fi
if [ "$NEW_VERSION" = "$CURRENT_REF" ]; then
log_info "Bereits auf dem neuesten Release ($CURRENT_REF)."
exit 0
fi
if [ "$AUTO_MODE" = true ]; then
log_info "Neues Release gefunden: $NEW_VERSION (aktuell: $CURRENT_REF) — automatisches Update wird gestartet."
fi
log_info "Update: $CURRENT_REF$NEW_VERSION"
# ---------------------------------------------------------------------------
# Backup
# ---------------------------------------------------------------------------
mkdir -p backups
BACKUP_FILE="backups/database-$(date +%Y-%m-%d-%H%M).sql.gz"
log_info "Erstelle Datenbank-Backup: $BACKUP_FILE"
set -a; source .env; set +a
if $COMPOSE ps -q postgres >/dev/null 2>&1 && [ -n "$($COMPOSE ps -q postgres)" ]; then
$COMPOSE exec -T postgres pg_dump -U "${POSTGRES_USER:-postgres}" "${POSTGRES_DB:-anouma}" | gzip > "$BACKUP_FILE"
log_success "Backup erstellt ($(du -h "$BACKUP_FILE" | cut -f1))."
else
log_warn "Datenbank-Container läuft nicht — Backup übersprungen."
fi
# Retention: delete backups older than BACKUP_RETENTION_DAYS, but never the
# one we just created.
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}"
find backups -name 'database-*.sql.gz' -mtime "+${RETENTION_DAYS}" -not -name "$(basename "$BACKUP_FILE")" -delete 2>/dev/null || true
# ---------------------------------------------------------------------------
# Deploy
# ---------------------------------------------------------------------------
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
}
deploy_ref() {
local ref="$1"
git checkout --quiet "$ref"
$COMPOSE build
$COMPOSE run --rm app npm run migrate
$COMPOSE up -d
}
log_info "Checke $NEW_VERSION aus und baue neu …"
if deploy_ref "$NEW_VERSION" && wait_healthy app 60; then
log_success "Update auf $NEW_VERSION erfolgreich."
echo "$NEW_VERSION" > .installed-version
exit 0
fi
log_error "Healthcheck nach Update auf $NEW_VERSION fehlgeschlagen — rolle Code auf $CURRENT_REF zurück."
log_warn "Datenbank-Migrationen werden dabei NICHT rückgängig gemacht (siehe DEPLOYMENT.md) — falls $NEW_VERSION eine nicht abwärtskompatible Migration enthielt, stelle das Backup manuell wieder her: $BACKUP_FILE"
if deploy_ref "$CURRENT_REF" && wait_healthy app 60; then
log_warn "Rollback auf $CURRENT_REF erfolgreich — die Anwendung läuft wieder auf der vorherigen Version."
exit 1
fi
fail "Rollback ebenfalls fehlgeschlagen — bitte manuell prüfen: $COMPOSE logs app"