Self-Hosting
Auf eurem Server, in einer Stunde
Ein Container, eine Datenbank, ein nginx davor. Diese Seite führt von einem leeren Server bis zur ersten angebundenen Anwendung.
Voraussetzungen
Was ihr braucht
-
Ein kleiner Server
Ein virtueller Server mit 1 vCPU und 1 GB Arbeitsspeicher genügt für eine Organisation mit einigen Dutzend Konten. Limen ist ein Dienst, der zwischendurch arbeitet, nicht durchgehend.
-
Docker und Docker Compose
Ein lokaler Build ist nicht nötig: Compose zieht das fertige Image aus der GitHub Container Registry und startet es zusammen mit PostgreSQL 16.
-
Eine Domain mit TLS
nginx terminiert TLS und reicht die Anfragen intern per HTTP weiter. Die öffentliche HTTPS-Adresse ist zugleich der OIDC-Issuer.
Installation
In drei Schritten zur laufenden Instanz
Beim ersten Hochfahren legt Limen die Datenbankstruktur, den ersten Verwaltungszugang und den Signaturschlüssel selbst an.
- 1
Holen und Geheimnisse erzeugen
Terminal# Repository holen – enthält docker-compose.yml und die .env-Vorlagegit clone https://github.com/leviora-studio/limen.git limen && cd limen# Konfiguration anlegencp .env.example .env# Geheimnisse erzeugen. Die Zahl hinter -hex ist die Länge in Byte,# die Ausgabe hat doppelt so viele Zeichen.openssl rand -hex 32 # -> AUTH_SECRET (64 Zeichen)openssl rand -hex 32 # -> ENCRYPTION_KEY (64 Zeichen)openssl rand -hex 16 # -> TRUSTED_PROXY_SECRET (32 Zeichen, optional) - 2
Konfiguration ausfüllen
Die Datei
.enventhält die wenigen Werte, die Limen wissen muss. Keiner der Platzhalter darf stehen bleiben..env# Öffentliche Adresse – zugleich OIDC-Issuer und WebAuthn-rpID.# Reine Laufzeit-Konfiguration: dasselbe Image läuft unter jeder Domain.APP_BASE_URL=https://id.beispiel.de# Geheimnisse aus dem vorigen SchrittAUTH_SECRET=<64 Hex-Zeichen>ENCRYPTION_KEY=<64 Hex-Zeichen># Datenbank – DATABASE_URL setzt Compose daraus selbst zusammenPOSTGRES_PASSWORD=<starkes Passwort># Erster Verwaltungszugang – Passwort nach dem ersten Login ändernADMIN_USER=erste.personADMIN_PASSWORD=<starkes Passwort># Empfohlen hinter nginx: verhindert gefälschte Client-AdressenTRUSTED_PROXY_SECRET=<32 Hex-Zeichen># Optional: Version festnageln statt :latestLIMEN_TAG=1.0.5 - 3
Starten und prüfen
Terminal# Datenbank und Anwendung starten. Compose lädt das fertige Image# ghcr.io/leviora-studio/limen – ein lokaler Build entfällt.# Beim ersten Hochfahren laufen Migrationen, der erste Verwaltungszugang# und der Signaturschlüssel automatisch durch.docker compose up -d# Läuft es?curl -s http://127.0.0.1:3000/api/health# {"status":"ok"}# Später aktualisieren – der Compose-Dienst steht auf pull_policy: always,# ein gesondertes docker compose pull braucht es nicht.docker compose up -d
nginx
TLS gehört davor
Limen spricht intern nur HTTP. nginx übernimmt die TLS-Terminierung, reicht die echte Herkunftsadresse durch und drosselt zusätzlich die Endpunkte für Anmeldung und Token.
- Die Anwendung ist nur an 127.0.0.1:3000 gebunden, nicht an die öffentliche Schnittstelle
- Der Host-Header muss unverfälscht ankommen – daran hängt der CSRF-Schutz aller Formulare
- X-Real-IP und die X-Forwarded-Header liefern die echte Herkunftsadresse für Drosselung und Protokoll
- limit_req auf /login und /token bremst Angriffsversuche schon vor der Anwendung ab
- Ein gemeinsames Geheimnis im Header X-Proxy-Auth verhindert gefälschte Herkunftsadressen
Eine vollständige Beispielkonfiguration liegt als nginx.conf.example im Repository.
# Gehört in http{}, nicht in server{}.# Zweite Linie vor der App-Drosselung.limit_req_zone $binary_remote_addr zone=sso_login:10m rate=5r/s;limit_req_zone $binary_remote_addr zone=sso_token:10m rate=10r/s; server { listen 443 ssl http2; server_name id.beispiel.de; ssl_certificate /etc/letsencrypt/live/id.beispiel.de/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/id.beispiel.de/privkey.pem; add_header Strict-Transport-Security "max-age=63072000" always; # Profilbilder bis 6 MB client_max_body_size 6m; # Auf server-Ebene – alle location-Blöcke erben diese Header. proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Proxy-Auth <TRUSTED_PROXY_SECRET>; # Anmeldung und Token-Endpunkt zusätzlich bremsen location = /login { limit_req zone=sso_login burst=10 nodelay; proxy_pass http://127.0.0.1:3000; } location = /token { limit_req zone=sso_token burst=20 nodelay; proxy_pass http://127.0.0.1:3000; } location / { proxy_pass http://127.0.0.1:3000; }} Anbinden
Eine Anwendung anschließen
Anwendung im Verwaltungsbereich registrieren, Discovery-URL in die
OIDC-Bibliothek eintragen, fertig. Das
client_secret wird genau einmal angezeigt.
- 1 Unter „Verwaltung → Anwendungen“ registrieren und die Redirect-URI zeichengenau eintragen
- 2 Client-ID und einmalig angezeigtes Secret in der Anwendung hinterlegen
- 3 Als Issuer die öffentliche Adresse eintragen – die Bibliothek findet die Endpunkte über das Discovery-Dokument selbst
- 4 Für eigene Anwendungen „first party“ setzen, damit der Zustimmungsschritt entfällt
// Beispiel: Auth.js als generischer OIDC-Provider{ id: 'limen', name: 'Limen', type: 'oidc', issuer: 'https://id.beispiel.de', clientId: process.env.LIMEN_CLIENT_ID, clientSecret: process.env.LIMEN_CLIENT_SECRET, authorization: { params: { scope: 'openid profile email' } },} Vor dem Livegang
Sieben Dinge, die man nicht übersehen sollte
Jeder Punkt hier hat einen Grund. Zwei davon – der Verschlüsselungsschlüssel und die Datensicherung – lassen sich nachträglich gar nicht mehr geradebiegen.
-
nginx muss den richtigen
HostdurchreichenDer CSRF-Schutz der Formulare hängt am eingebauten Abgleich von Origin und Host – nicht an APP_BASE_URL. Kommt der falsche Host an, blockiert jede Formularsendung: Anmeldung, Konto, Verwaltung. Das ist der häufigste Grund für ein Formular, das scheinbar grundlos nichts tut.
-
ENCRYPTION_KEYsichern – getrennt von der DatenbankMit diesem Schlüssel sind die Signaturschlüssel und TOTP-Secrets verschlüsselt. Geht er verloren, lassen sie sich nicht wiederherstellen. Ein Wechsel ist nur über das mitgelieferte Rotations-Skript möglich.
-
APP_BASE_URLmuss die echte öffentliche Adresse seinSie wird beim Start aus der Umgebung gelesen – das Image selbst ist von der Domain unabhängig. Aus ihr leiten sich der Issuer, die Rückleitungen und die rpID für Passkeys ab. Ein falscher Wert bricht Tokens und Anmeldungen; nach einer Änderung den Container neu starten.
-
Die Anwendung nie direkt erreichbar machen
Compose bindet sie bewusst nur an 127.0.0.1:3000. Ein Zugriff an nginx vorbei würde gefälschte Herkunftsadressen erlauben – und damit Drosselung und Protokoll aushebeln. TRUSTED_PROXY_SECRET schiebt dem zusätzlich einen Riegel vor.
-
Sicherungen für
./pgdataeinplanenDie Konten liegen in PostgreSQL, das Verzeichnis liegt neben der Compose-Datei. Ohne Sicherung ist ein Serverausfall gleichbedeutend mit dem Verlust aller Zugänge. Die Profilbilder liegen daneben in ./uploads.
-
Für den Produktivbetrieb eine Version festnageln
Ohne Angabe läuft :latest, das sich bei jedem Push auf main ändert. Mit LIMEN_TAG=1.0.5 in der .env bleibt der Stand stabil, bis ihr ihn bewusst anhebt.
-
Den ersten Verwaltungszugang absichern
Nach der ersten Anmeldung das Bootstrap-Passwort ändern und einen zweiten Faktor einrichten. Ohne ihn bleibt der Verwaltungsbereich ohnehin gesperrt.
Lizenz: AGPL-3.0
Nutzen, betreiben und ändern ist ausdrücklich erlaubt. Wer eine geänderte Fassung weitergibt oder als Netzdienst betreibt, muss den Quellcode einschließlich der Änderungen unter derselben Lizenz zugänglich machen. Eine geschlossene Variante kann so nicht entstehen.
Für den internen Betrieb in der eigenen Organisation entstehen daraus keine zusätzlichen Pflichten.