Installation¶
Abstimmbar ist freie Software (Apache-2.0) und läuft als Docker-Compose-Stack auf einem eigenen Linux-Server: PostgreSQL, Django (uvicorn) und Caddy als Reverse Proxy mit automatischem HTTPS. Die Anmeldung der Lehrenden läuft über den Identity Provider Ihrer Einrichtung (OIDC, z. B. Keycloak); Teilnehmende brauchen kein Konto.
Quelle dieser Seite
Der folgende Text ist die Betriebsanleitung aus dem Repository
(docs/deployment.md)
und wird bei jedem Website-Build von dort übernommen. Die Einbindung in
Lernplattformen wie Moodle beschreibt die Seite
LMS-Einbindung (LTI 1.3).
Abstimmbar läuft produktiv als Docker-Compose-Stack nach dem Vorbild von AusleihBar: PostgreSQL + Django (uvicorn/ASGI) + Caddy als TLS-terminierender Reverse Proxy. Kein Keycloak im Stack — OIDC zeigt auf den institutionellen IdP.
Browser ── https ──> Caddy ──> /api, /admin, /oidc, /lti, /p → uvicorn (Django)
│ /static, /media → Volumes (file_server)
│ alles andere → SPA-Build (Volume frontend_data)
└── automatisches TLS (Let's Encrypt) oder eigene Zertifikate
Das app-Image (Dockerfile im Repo-Root) bringt die fertig gebaute SPA
bereits mit (mehrstufiger Build: Node baut das Frontend, die Stufe danach
enthält Django) — beim Start kopiert der Container sie ins Volume
frontend_data, aus dem Caddy sie direkt ausliefert. Ein separater
Frontend-Build-Schritt auf dem Host entfällt dadurch.
Der erste Teil ist eine vollständige Schritt-für-Schritt-Anleitung für Rocky Linux 9, dem folgen Keycloak-/LTI-Konfiguration, Betriebshinweise, Alltagskommandos, Updates, Zertifikate und Backup.
Installation auf Rocky Linux 9 — Schritt für Schritt¶
Eine komplette Anleitung für einen frischen Rocky-Linux-9-Server, so geschrieben, dass sie ohne Linux-Vorerfahrung nachvollziehbar ist. Kopieren Sie jeden Befehl exakt.
Wie es zusammenspielt (einmal lesen): Auf dem Server wird nur eine
Sache installiert — Docker. Docker lädt und betreibt alle Bausteine als
Container: die Datenbank (PostgreSQL), die Anwendung (Django) und
den Webserver mit HTTPS (Caddy). PostgreSQL und Caddy werden nicht
von Hand installiert, und Caddy besorgt sich das HTTPS-Zertifikat von
Let's Encrypt automatisch. Alles ist unter einer Adresse erreichbar, z. B.
https://abstimmbar.example.org — Teilnehmende nutzen
https://abstimmbar.example.org/p/ bzw. den QR-Code aus dem
Präsentationsmodus.
Vorher benötigt:
- Ein Server mit Rocky Linux 9 und ein Benutzer mit
sudo-Rechten (oderroot— dann dassudovor den Befehlen weglassen). - Ein Domainname (z. B.
abstimmbar.example.org), für den Sie einen DNS-Eintrag anlegen können. HTTPS braucht eine echte Domain. - Die öffentliche IP-Adresse des Servers.
- Ein OIDC-Client im institutionellen Keycloak (Details unten; kann auch nachgereicht werden, der Stack startet ohne funktionierenden Login).
Notation: Zeilen mit
sudowerden im Terminal des Servers ausgeführt. Im Editornanospeichern Sie mit Strg+O, Enter und verlassen ihn mit Strg+X.
Schritt 1 — Am Server anmelden¶
Vom eigenen Rechner aus:
Schritt 2 — System aktualisieren und Grundwerkzeuge installieren¶
Hat dnf update einen neuen Kernel installiert (typisch bei einer frischen
VM), dann jetzt einmal neu starten, damit der neue Kernel samt passender
Module läuft — sonst kann Docker in Schritt 3 seine Netzwerk-Regeln nicht
setzen (siehe Troubleshooting):
Schritt 3 — Docker installieren¶
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
sudo docker --version # gibt eine Version aus → Docker läuft
(Optional, um das sudo vor docker loszuwerden:
sudo usermod -aG docker $USER, dann ab- und wieder anmelden. Diese
Anleitung behält sudo bei, damit sie so oder so funktioniert.)
Schritt 4 — Firewall für Web-Verkehr öffnen¶
Caddy braucht Port 80 (für das Zertifikat und die Umleitung auf HTTPS) und 443 (HTTPS).
Rocky nutzt firewalld; bei einer Minimalinstallation fehlt es unter Umständen. Erst installieren/aktivieren (ohne Wirkung, falls schon da):
Dann die beiden Web-Ports öffnen und neu laden:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-services # muss 'http' und 'https' enthalten
Hinweis: Manche Server (v. a. Cloud-/Rechenzentrums-VMs) haben keine Host-Firewall, sondern filtern über eine externe Security Group / Netz-Firewall. Wenn
firewall-cmdtrotz der Schritte obenFirewallD is not runningmeldet — oder Sie bewusst kein firewalld nutzen — überspringen Sie diesen Schritt und lassen Sie stattdessen die Ports 80 und 443 in der externen Firewall freischalten.
Schritt 5 — Domain auf den Server zeigen lassen (DNS)¶
Beim DNS-Verantwortlichen (an der Uni: das Rechenzentrum) einen A-Record der Domain auf die öffentliche IP des Servers anlegen lassen (und einen AAAA-Record, falls IPv6 vorhanden). Danach vom Server aus prüfen:
sudo dnf -y install bind-utils # liefert den Befehl 'dig'
dig +short abstimmbar.example.org # muss die IP des Servers ausgeben
Nicht weitermachen, bevor hier die richtige IP erscheint — Let's Encrypt prüft genau das.
Schritt 6 — Anwendung herunterladen¶
sudo mkdir -p /opt && cd /opt
sudo git clone https://github.com/virtUOS/abstimmbar.git
cd abstimmbar
(Das Repository ist öffentlich — weder SSH-Key noch Zugangsdaten nötig.)
Schritt 7 — Konfigurationsdatei anlegen¶
Secret Key erzeugen (Pflicht)¶
Django signiert damit Sitzungen und Tokens. Jede Installation braucht einen
eigenen, zufälligen Schlüssel — der Platzhalter aus .env.prod.example
(change-me-to-a-long-random-string) ist öffentlich bekannt, wer ihn kennt,
könnte Sitzungen fälschen. Erzeugen Sie einen Schlüssel:
Abstimmbar verweigert den Start mit dem Platzhalter. Im Log der App steht dann (siehe Bestehende Installation aktualisieren):
django.core.exceptions.ImproperlyConfigured: DJANGO_SECRET_KEY must be set when DJANGO_DEBUG=0 (the dev default and the .env.prod.example placeholder are refused)
Fehlt der Schlüssel ganz oder ist er leer, bricht schon Docker Compose vor dem Start ab:
Den Schlüssel später zu ändern ist unkritisch, hat aber zwei Folgen: Alle Angemeldeten werden abgemeldet, und offene „Rückgängig"-Tokens der KI-Funktionen (24 Stunden gültig) verfallen. Daten gehen dabei nicht verloren.
Werte eintragen¶
In nano mindestens diese Werte setzen (jedes ... und die
Beispiel-Domain ersetzen; den eben erzeugten Secret Key einfügen):
# Öffentliche Adresse der Instanz (zweimal derselbe Wert). Nur Schema +
# Host — kein Pfad, kein abschließender Schrägstrich. Zugleich die
# Site-Adresse für Caddy (Schritt 8).
PUBLIC_BASE_URL=https://abstimmbar.example.org
CSRF_TRUSTED_ORIGINS=https://abstimmbar.example.org
# Kontakt-E-Mail für Let's Encrypt (Ablauf-Hinweise zum Zertifikat)
ACME_EMAIL=admin@example.org
# Django
DJANGO_SECRET_KEY=HIER-DEN-ERZEUGTEN-SECRET-KEY-EINFUEGEN
DJANGO_ALLOWED_HOSTS=abstimmbar.example.org
# Datenbank — ein langes Zufallspasswort wählen (z. B. openssl rand -base64 24)
POSTGRES_PASSWORD=hier-ein-starkes-passwort
# Login (OIDC) — institutioneller Keycloak. OIDC_OP_ISSUER genügt, die
# Endpunkte kommen per Discovery; Details im Abschnitt "Keycloak-Client".
OIDC_RP_CLIENT_ID=abstimmbar
OIDC_RP_CLIENT_SECRET=...
OIDC_OP_ISSUER=https://sso.example.org/realms/example
# Mitglieder dieser IdP-Gruppe bekommen beim Login Django-Admin (leer = aus).
OIDC_ADMIN_GROUP=abstimmbar-admins
Speichern mit Strg+O, Enter, beenden mit Strg+X.
Schritt 8 — Webserver (Caddy) konfigurieren¶
Wie Caddy installiert wird: gar nicht per dnf — Caddy läuft als
Docker-Container aus dem offiziellen caddy:2-Image, das Docker beim
Start des Stacks (Schritt 10) automatisch lädt. Definiert ist es in
docker-compose.prod.yml (Service caddy); dort öffnet es die Ports
80/443 und legt seine Zertifikate im Volume caddy_data ab.
Wo die Konfiguration liegt: in der Datei Caddyfile im Projektordner
(/opt/abstimmbar/Caddyfile), die das Compose-File read-only in den
Container hängt. Sie gehört zum Repository und wird nicht bearbeitet —
alles Installationsspezifische kommt aus der .env (Schritt 7), die
docker-compose.prod.yml an den Caddy-Container durchreicht:
| Variable | Bedeutung |
|---|---|
PUBLIC_BASE_URL |
Site-Adresse, für die Caddy das Zertifikat holt — derselbe Wert wie für die App, nur Schema + Host (https://abstimmbar.example.org, kein Pfad, kein abschließender /). https:// macht die Seite HTTPS-only; HTTP leitet Caddy um. |
ACME_EMAIL |
Kontaktadresse für Let's Encrypt (Ablauf-Hinweise). Pflicht. |
ABSTIMMBAR_FRAME_ANCESTORS |
Optional, nur für LTI im iframe: LMS-Origins, die die Oberfläche einbetten dürfen, durch Leerzeichen getrennt, z. B. "https://moodle.example.org https://studip.example.org". Leer (Standard) = keine fremde Einbettung. |
Zertifikate richten Sie nicht von Hand ein — Caddy besorgt und
erneuert das Let's-Encrypt-Zertifikat automatisch, sobald Domain und Ports
stimmen. Geänderte Werte in der .env übernehmen Sie mit
sudo docker compose -f docker-compose.prod.yml up -d (erstellt Caddy neu;
ein bloßes restart liest die .env nicht neu ein).
Weil das Caddyfile unverändert bleibt, läuft ein späteres git pull
ohne Konflikte durch (siehe
Bestehende Installation aktualisieren).
Die vorgesehenen Ausnahmen: die tls-Zeile für eigene Zertifikate der
Hochschule bzw. einen rein internen Host (siehe
HTTPS-Zertifikate) und optional die
IP-Einschränkung für /metrics (siehe monitoring.md).
An Django weitergereicht werden die Pfade /api/, /admin/, /oidc/,
/lti/, /p/, /r/, /c/, /question-preview/ und /metrics;
/static/ und /media/ liefert Caddy direkt aus den Volumes static_data und
media_data aus; alles andere liefert die Oberfläche aus. Setzen Sie einen eigenen
Reverse-Proxy ein, muss er beides genauso handhaben.
Schritt 9 — Dateien für SELinux freigeben¶
Rocky Linux hat SELinux aktiviert; ohne Label darf ein Container keine vom
Host gemounteten Dateien lesen. Den Pfad freigeben, den Caddy liest
(frontend/dist gibt es nicht mehr — die SPA liegt jetzt in einem
Docker-Volume, das keine Host-Freigabe braucht):
Schritt 10 — Alles starten¶
Entweder aus dem Quellcode bauen (Node baut die SPA, Django-Image entsteht direkt danach — kein separater Frontend-Build-Schritt mehr nötig):
… oder ein veröffentlichtes Release-Image laden (kein lokaler Build nötig,
schneller; ABSTIMMBAR_VERSION auf den Namen des Release-Tags setzen, z. B. v1.2.0 (die Images gibt es gleichlautend auch ohne führendes „v“), sonst
wird latest verwendet):
sudo docker compose -f docker-compose.prod.yml pull
sudo docker compose -f docker-compose.prod.yml up -d
Das lädt PostgreSQL und Caddy, führt die Datenbank-Migrationen aus und startet alle Dienste. Caddy holt das HTTPS-Zertifikat (dauert bis zu einer Minute). Logs beobachten:
Danach https://abstimmbar.example.org im Browser öffnen — die
Startseite der Verwaltung erscheint; https://abstimmbar.example.org/p/
zeigt die Code-Eingabe für Teilnehmende.
Schritt 11 — Erstes Admin-Konto anlegen¶
Ein lokales Break-Glass-Konto, unabhängig vom IdP (nützlich, falls OIDC mal nicht erreichbar ist):
Den Eingaben folgen (Benutzername, E-Mail, Passwort); Login danach unter
https://abstimmbar.example.org/admin/. Dieses Konto bleibt rein
passwortbasiert unter /admin/ und wird von keinem OIDC-Login übernommen.
Lehrende melden sich regulär per
Uni-Login (OIDC) auf der Startseite an; Mitglieder der Gruppe aus
OIDC_ADMIN_GROUP bekommen dabei automatisch Django-Admin-Rechte.
Übersteht das einen Server-Neustart?¶
Ja — das Setup ist reboot-sicher:
- Docker startet beim Boot (
systemctl enable docker, Schritt 3). - Alle Dienste haben
restart: unless-stopped— Datenbank, Backend und Caddy kommen nach einem Reboot von selbst wieder hoch (außer Sie haben den Stack bewusst mit… downgestoppt). - Alle Daten liegen in persistenten Volumes: Datenbank (
postgres_data), hochgeladene Bilder (media_data), Zertifikate (caddy_data). Beim Start laufen Migrationen undcollectstaticautomatisch erneut. - Die Firewall-Regeln wurden mit
--permanentgespeichert (Schritt 4).
Prüfen:
sudo reboot
# nach dem Neustart wieder anmelden, dann:
cd /opt/abstimmbar
sudo docker compose -f docker-compose.prod.yml ps # alle Dienste "running"
Keycloak-Client (institutioneller IdP)¶
Client-Einstellungen (Confidential Client, Standard Flow):
- Redirect URI:
https://<domain>/oidc/callback/ - Post-Logout-Redirect URI:
https://<domain>/ - Backchannel-Logout-URL:
https://<domain>/oidc/backchannel-logout/ - Group-Membership-Mapper auf den Claim
groups(fürOIDC_ADMIN_GROUP)
In .env reicht OIDC_OP_ISSUER (Endpunkte kommen per Discovery);
abweichende Endpunkte lassen sich per OIDC_OP_* explizit setzen.
LTI (Moodle/Stud.IP)¶
Registrierung der LMS-Plattformen wie in lti.md beschrieben; die
Tool-Endpunkte liegen unter https://<domain>/lti/…. Empfohlen ist der
Start im neuen Fenster. Nur für iframe-Betrieb in .env
SESSION_COOKIE_SAMESITE=None setzen (erzwingt Secure-Cookies) und
ABSTIMMBAR_FRAME_ANCESTORS auf die LMS-Origin(s) setzen (siehe Schritt 8),
dann sudo docker compose -f docker-compose.prod.yml up -d.
Monitoring (Prometheus/Grafana)¶
Die Nutzungskennzahlen der Admin-Statistik stehen optional unter
https://<domain>/metrics für Prometheus bereit (aus, solange
METRICS_TOKEN nicht gesetzt ist). Einrichtung, Kennzahlen und
Beispielabfragen: monitoring.md.
Betriebshinweise¶
- Ein Backend-Prozess, kein Scale-out: Der SSE-Hub (ADR-0003) lebt im
Prozessspeicher. Mehrere uvicorn-Worker oder Replikas würden die
Verbindungen aufteilen — Stimmen aus Worker A kämen nie bei Geräten an,
die mit Worker B verbunden sind. Ein Prozess trägt ≥1000 Teilnehmende
(Lasttest
scripts/loadtest.py); mehr braucht erst einen externen Broker (z. B. Redis Pub/Sub) — bewusst aufgeschoben. - Dateideskriptoren: SSE hält pro Teilnehmergerät einen Socket offen;
das Compose-File setzt
nofileauf 65536. - Graceful Shutdown:
--timeout-graceful-shutdown 5ist gesetzt, weil offene SSE-Streams nie von selbst enden — ohne das Flag hinge jeder Neustart. - DB-Verbindungen: Der psycopg-Pool (settings.py) deckelt bei 20 Verbindungen; PostgreSQLs Default (100) braucht keine Anpassung.
- Modus-Statistik (Datenschutz): Für die Kennzahl „Sitzungen pro Tag
nach Easy/Pro-Modus“ legt
whoamije Sitzung und Tag eine Zeile inDailyModeSessionan — gespeichert wird nur ein SHA-256-Hash des Session-Keys (nie der rohe Key), passend zur Anonymitäts-Zusage. Die Tabelle wächst dadurch pro Tag um grob eine Zeile je aktiver Sitzung; siehe „Retention“ unten. - Rundgang-Statistik (Datenschutz): Starts, Abschlüsse und Abbrüche des
Rundgangs werden nur als Tageszähler in
TourDailyCounterfasst (Tag, Modus, Startquelle bzw. Abbruchschritt, Anzahl) — ohne Personen- oder Sitzungsbezug und ohne Zeitstempel. Die Tabelle bleibt klein (wenige Zeilen pro Tag). Beispielräume zählen nicht in die Nutzungszahlen.
Retention der Modus-Statistik¶
Die Tabelle DailyModeSession wächst kontinuierlich. Ein
Management-Command löscht Zeilen, die älter als N Tage sind (Default 400,
also gut ein akademisches Jahr Vorlauf):
cd /opt/abstimmbar
P="sudo docker compose -f docker-compose.prod.yml"
$P exec app python manage.py prune_mode_sessions # Default: 400 Tage
$P exec app python manage.py prune_mode_sessions --days 180 # kürzeres Fenster
Empfohlen als wöchentlicher Cron-Job auf dem Host, z. B. in
/etc/cron.d/abstimmbar-prune (Sonntag 03:30):
30 3 * * 0 root cd /opt/abstimmbar && docker compose -f docker-compose.prod.yml exec -T app python manage.py prune_mode_sessions >/dev/null 2>&1
Der Job ist optional — er hält nur die Tabelle klein; ohne ihn bleibt die Anwendung funktionsfähig, sammelt aber unbegrenzt (gehashte) Zeilen.
Alltagskommandos¶
Alle im Ordner /opt/abstimmbar ausführen:
P="sudo docker compose -f docker-compose.prod.yml"
$P ps # was läuft
$P logs -f # Live-Logs (Strg+C beendet)
$P restart # Anwendung neu starten
$P down # alles stoppen
$P up -d --build # starten / Änderungen übernehmen
Einen periodischen Job gibt es: die optionale Retention der Modus-Statistik (siehe Abschnitt „Retention der Modus-Statistik“). Sonst braucht Abstimmbar keine geplanten Wartungsaufgaben.
Bestehende Installation aktualisieren¶
Vorher: Die Release-Hinweise seit Ihrer Version lesen, auf
https://github.com/virtUOS/abstimmbar/releases (ab 1.0 zusätzlich die
CHANGELOG.md im Repository) — besonders die Hinweise für den Betrieb
(„Betrieb"/„Breaking", z. B. neue Pflicht-Variablen in der .env).
Was Ihnen gehört, was aus git kommt: Ihre .env und ggf. Ihre
Zertifikate (/etc/abstimmbar/certs/) bleiben unberührt. Alles andere —
auch Caddyfile und docker-compose.prod.yml — kommt aus git und wird
nicht lokal geändert (bis auf die vorgesehenen Ausnahmen aus Schritt 8).
1. Neuen Stand holen. Aus dem Quellcode bauen:
cd /opt/abstimmbar
sudo git pull
sudo docker compose -f docker-compose.prod.yml build
sudo docker compose -f docker-compose.prod.yml up -d
Oder ein veröffentlichtes Release-Image nutzen (kein lokaler Build). Auch
hier zuerst git pull, weil docker-compose.prod.yml und Caddyfile aus
dem Repository kommen. Das Image-Tag latest bewegt sich nur, wenn ein
neues Release veröffentlicht wurde; wer ABSTIMMBAR_VERSION in der .env
fest eingetragen hat (z. B. v1.3.0), setzt dort die neue Version und
checkt statt git pull das passende Tag aus (sudo git fetch --tags &&
sudo git checkout v1.3.0), damit docker-compose.prod.yml, Caddyfile
und Image zueinander passen:
Was sich in einer Version geändert hat und was beim Update zu beachten ist, steht im CHANGELOG und in den Release Notes – lesen Sie vor allem die „Hinweise für den Betrieb“, bevor Sie aktualisieren. Die laufende Version sehen Sie in der Fußzeile der Anwendung und im Admin-Bereich.
cd /opt/abstimmbar
sudo git pull
sudo docker compose -f docker-compose.prod.yml pull
sudo docker compose -f docker-compose.prod.yml up -d
Datenbank-Migrationen und collectstatic laufen beim Start automatisch —
nichts davon von Hand ausführen.
git pull bricht ab wegen lokaler Änderungen? Das passiert, wenn
Dateien aus dem Repository auf dem Server bearbeitet wurden (früher
typisch: Domain und E-Mail im Caddyfile — heute Variablen in der .env,
siehe Schritt 8). Alte Anpassungen dieser Art in die .env übertragen und
die Datei zurücksetzen (sudo git checkout -- Caddyfile). Nur die
vorgesehenen Ausnahmen (tls-Zeile samt Zertifikats-Mount, siehe
HTTPS-Zertifikate; optional die
/metrics-IP-Einschränkung) tragen Sie über das Update:
Meldet git stash pop einen Konflikt, die betroffene Datei (meist das
Caddyfile) prüfen, die neue Fassung aus git behalten, Ihre tls-Zeile
wieder einfügen und den Stash danach mit sudo git stash drop verwerfen.
2. Prüfen, ob die Anwendung läuft:
Bereit ist sie, sobald Application startup complete erscheint. Die zwei
typischen Startverweigerungen:
- Secret Key ist der Platzhalter: im App-Log
ImproperlyConfigured: DJANGO_SECRET_KEY must be set when DJANGO_DEBUG=0 …— einen eigenen Schlüssel erzeugen und eintragen (siehe Secret Key erzeugen), dann erneutup -d. - Pflicht-Variable fehlt in der
.env(z. B.DJANGO_SECRET_KEYoderACME_EMAIL): Docker Compose bricht schon vor dem Start ab, z. B.required variable DJANGO_SECRET_KEY is missing a value: set DJANGO_SECRET_KEY— die genannte Variable in der.envergänzen (Muster in.env.prod.example), dann erneutup -d.
3. Browser neu laden. Bereits geöffnete Tabs behalten die alte Oberfläche, bis die Seite hart neu geladen wird (Strg+Umschalt+R, am Mac Cmd+Umschalt+R). Teilen Sie das ggf. Lehrenden mit, die die App während des Updates offen hatten.
Neue Konfigurations-Variable? An drei Stellen nachziehen¶
docker-compose.prod.yml nutzt kein env_file — jede Variable wird
einzeln unter environment: mit ${VAR} durchgereicht. Eine Variable, die
nur in der .env steht, aber nicht im environment:-Block, erreicht den
Container nie. Bringt ein Update also eine neue Einstellung mit, an drei
Stellen ergänzen:
docker-compose.prod.yml→environment:desapp-Service (bzw.caddyfür Proxy-Einstellungen; die leicht vergessene Stelle),.env.prod.example(Doku),- die echte
.envauf dem Server, dannup -d(erstellt neu, was sich geändert hat).
VITE_API_BASE_URL ist ein Sonderfall und im Normalfall nicht
nötig: die SPA nutzt standardmäßig relative URLs (gleicher Origin wie die
API, hinter Caddy). Nur für einen bewusst abweichenden Aufbau (SPA und API
auf unterschiedlichen Origins) setzen. Vite backt den Wert dann zur
Buildzeit fest ins Bundle — das funktioniert nur, wenn Sie selbst bauen
(up -d --build); ein veröffentlichtes Release-Image wurde bereits mit dem
Standardwert gebaut und lässt sich nicht nachträglich umkonfigurieren.
VITE_GUIDE_URL (optional) ist das Ziel des Menüpunkts „Anleitung“ im
„?“-Menü der App bei deutscher Oberfläche; Standard ist die öffentliche
Projekt-Website (https://virtuos.github.io/abstimmbar/anleitung/).
VITE_GUIDE_URL_EN (optional) ist dasselbe für die englische Oberfläche
(Standard https://virtuos.github.io/abstimmbar/en/guide/). Wie
VITE_API_BASE_URL werden beide Werte zur Buildzeit festgelegt
(Build-Argumente im app-Service, nur mit up -d --build wirksam).
Gegenprüfen, was im laufenden Container ankommt:
sudo docker compose -f docker-compose.prod.yml exec app env \
| grep -oE "^(DJANGO_|OIDC_|POSTGRES_|AI_|CSRF|FRONTEND|SESSION)[A-Z_]*" | sort
HTTPS-Zertifikate (Caddy), im Klartext¶
- Caddy besorgt und erneuert automatisch ein kostenloses
Let's-Encrypt-Zertifikat für den Host aus
PUBLIC_BASE_URL. Nichts von Hand zu tun. Voraussetzungen: Ports 80 und 443 aus dem Internet erreichbar, korrektes DNS (Schritt 5) und eine gültigeACME_EMAILin der.env. - Die Zertifikate liegen im Docker-Volume
caddy_data— mitsichern, damit sie einen Neuaufbau überleben (sonst stellt Caddy sie einfach neu aus, was aber den Let's-Encrypt-Rate-Limits unterliegt). - Keine öffentliche Domain (rein interner Host)?
PUBLIC_BASE_URLauf den internen Namen setzen (z. B.https://abstimmbar.intern, wie immer ohne Pfad und ohne abschließenden/) und imCaddyfileinnerhalb des Site-Blocks die Zeiletls internalergänzen — Caddy nutzt dann seine eigene lokale CA statt Let's Encrypt. - Lokale
tls-Zeile = bewusste Ausnahme. DasCaddyfilewird sonst nie bearbeitet. Vorgesehene Ausnahmen sind dietls-Zeile (hier oder für eigene Zertifikate, siehe unten) und optional die IP-Einschränkung für/metrics(siehe monitoring.md); bei Updates tragen Sie sie mitsudo git stash && sudo git pull && sudo git stash pophinüber (siehe Bestehende Installation aktualisieren).
Eigenes Zertifikat (z. B. HARICA) statt Let's Encrypt¶
Wenn die Hochschule das Zertifikat ausstellt (HARICA, DFN, interne CA …), zeigt Caddy auf die Dateien statt auf automatisches HTTPS:
- Die beiden PEM-Dateien auf dem Host ablegen, außerhalb des Repos, damit
der private Schlüssel nie committet wird, z. B. unter
/etc/abstimmbar/certs/: abstimmbar.pem— Serverzertifikat mit Zwischenzertifikat(en) (Leaf zuerst, dann die CA-Kette), PEM-Format.abstimmbar.key— der passende private Schlüssel, PEM, ohne Passphrase (Caddy kann keine abfragen).- Das Verzeichnis in den Caddy-Container mounten —
docker-compose.prod.ymlenthält die (auskommentierte) Zeile dafür: - Im
Caddyfileinnerhalb des Site-Blocks ({$PUBLIC_BASE_URL} {) einetls-Zeile mit den Container-Pfaden ergänzen (Zertifikat zuerst, dann Schlüssel). Der Host inPUBLIC_BASE_URLmuss zum Zertifikat passen. Diese Zeile und die einkommentierte Mount-Zeile aus Punkt 2 sind vorgesehene lokale Änderungen — bei Updates mitgit stash/git stash popmitnehmen (siehe oben): - Das Verzeichnis für SELinux labeln (Rocky), damit der Container lesen darf:
- Übernehmen mit
sudo docker compose -f docker-compose.prod.yml up -d(oderrestart caddy). Die Erneuerung ist in diesem Modus manuell: Dateien vor Ablauf ersetzen, dannrestart caddy.ACME_EMAILwird dabei nicht genutzt, muss aber trotzdem gesetzt sein (Pflicht-Variable).
Backup¶
Die Daten liegen in den Volumes postgres_data (Datenbank) und
media_data (hochgeladene Bilder); caddy_data (Zertifikate) ist
verzichtbar, erspart aber Neuausstellungen:
cd /opt/abstimmbar
sudo docker compose -f docker-compose.prod.yml exec db \
pg_dump -U abstimmbar abstimmbar | gzip > abstimmbar-$(date +%F).sql.gz
sudo docker run --rm -v abstimmbar_media_data:/data -v "$PWD":/backup alpine \
tar czf /backup/abstimmbar-media-$(date +%F).tar.gz -C /data .
Die Dumps regelmäßig (z. B. per Cron) erzeugen und auf ein anderes System kopieren.
Troubleshooting¶
Docker startet nicht: fehlende Netfilter-Kernel-Module¶
Symptom. sudo systemctl enable --now docker schlägt fehl; in
sudo journalctl -u docker --no-pager stehen Zeilen wie:
iptables ... -m addrtype ... Warning: Extension addrtype revision 0 not supported, missing kernel module?
iptables ... RULE_APPEND failed (No such file or directory)
failed to start daemon: Error initializing network controller ...
Ursache. Docker legt beim Start seine NAT-Regeln an und braucht dafür
Kernel-Module wie xt_addrtype, br_netfilter und nf_nat. Auf
Minimal- oder Cloud-Images (z. B. OpenStack/Nova) fehlt oft das Paket
kernel-modules, oder dnf update (Schritt 2) hat einen neuen Kernel
installiert, ohne dass neu gebootet wurde — dann passt das laufende
Kernel-Verzeichnis nicht zu den vorhandenen Modulen.
Prüfen. Bestätigt die Diagnose, wenn hier „Module not found" erscheint:
Beheben. Passende Kernel-Module nachinstallieren und neu starten (der Reboot bringt zugleich den nach dem Update aktuellen Kernel ans Laufen):
Nach dem Reboot die fehlgeschlagene Startsperre lösen und Docker starten:
sudo systemctl reset-failed docker.service
sudo systemctl enable --now docker
sudo docker run --rm hello-world # muss "Hello from Docker!" ausgeben
Meldet dnf, kernel-modules-$(uname -r) sei bereits installiert oder es
gebe keinen Treffer, liegt es rein am Kernel/Modul-Versatz nach dem Update —
dann genügt der Reboot allein (das dnf install überspringen).
Kein HTTPS: ERR_SSL_PROTOCOL_ERROR / Let's-Encrypt-Timeout (Firewall)¶
Symptom. Der Browser meldet ERR_SSL_PROTOCOL_ERROR, und in den
Caddy-Logs scheitert die Zertifikatsausstellung:
challenge failed ... "detail":"<IP>: Timeout during connect (likely firewall problem)"
could not get certificate from issuer ... acme:error:connection
Ursache. Let's Encrypt validiert per http-01 (eingehend Port 80) bzw.
tls-alpn-01 (eingehend Port 443). Beide Challenges müssen den Server aus
dem öffentlichen Internet erreichen. Kommt das Timeout, ist eingehend
80/443 geblockt. Ohne Zertifikat kann Caddy auf 443 kein gültiges TLS
ausliefern → ERR_SSL_PROTOCOL_ERROR. Auf OpenStack-VMs gibt es dafür
zwei mögliche Sperren: die Security-Group der Instanz (Standard
lässt oft nur SSH/22 zu) und den Perimeter-Firewall des Rechenzentrums.
Prüfen. DNS zeigt auf eine öffentliche IP (dig +short <domain>), die
Ports lauschen lokal (sudo ss -tlnp | grep -E ':80|:443') — aber ein Test
von außerhalb des Uni-Netzes (z. B. Handy im Mobilfunknetz) läuft ins Leere.
Beheben.
1. Security-Group öffnen (in Horizon: Netzwerk → Sicherheitsgruppen →
Regel hinzufügen — Ingress TCP 80 und 443 aus 0.0.0.0/0; optional
UDP 443 für HTTP/3), oder per CLI:
openstack security group rule create --proto tcp --dst-port 80 --remote-ip 0.0.0.0/0 <sg>
openstack security group rule create --proto tcp --dst-port 443 --remote-ip 0.0.0.0/0 <sg>
sudo docker compose -f docker-compose.prod.yml restart caddy
sudo docker compose -f docker-compose.prod.yml logs -f caddy | grep -iE "obtain|certificate|error"
certificate obtained successfully.
Hinweis: Wiederholte Fehlversuche zählen auf Let's Encrypts Limit „5 fehlgeschlagene Validierungen pro Konto/Hostname/Stunde". Erst die Freigabe klären, dann neu starten — nicht in Schleife, solange 80/443 noch dicht sind. Bei
rateLimited~1 Stunde warten.Kann der Dienst gar nicht öffentlich erreichbar sein, ist statt Let's Encrypt ein HARICA-Zertifikat der Weg (siehe oben, Eigenes Zertifikat) — es braucht keinen eingehenden Internet-Zugriff.
Sicherheits-Checkliste¶
- [ ]
DJANGO_DEBUGist im Prod-Compose fest auf0; eigener, zufällig erzeugterDJANGO_SECRET_KEY(nicht der Platzhalter, siehe Secret Key erzeugen); echtesDJANGO_ALLOWED_HOSTS. - [ ]
PUBLIC_BASE_URLundCSRF_TRUSTED_ORIGINSzeigen auf die echtehttps://-Domain (ohne Pfad, ohne abschließenden/) (VITE_API_BASE_URLnur bei bewusst abweichendem Aufbau, siehe „Neue Konfigurations-Variable?"). - [ ] Starkes
POSTGRES_PASSWORD; alle Geheimnisse nur in.env(wird nie committet). - [ ] OIDC zeigt auf den institutionellen IdP;
/oidc/callback/und die Backchannel-Logout-URL sind dort registriert. - [ ] Regelmäßige Backups von
postgres_dataundmedia_data. - [ ] Nur die Ports 80/443 sind offen; die Services
dbundappsind nicht veröffentlicht (exposestattports). - [ ] Einmal geprüft, dass der Stack nach
sudo rebootvon selbst wiederkommt.