Zum Inhalt springen
Limen

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. 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. 2

    Konfiguration ausfüllen

    Die Datei .env enthä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. 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.

nginx.conf · http{}
# 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;
nginx.conf · server{}
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. 1 Unter „Verwaltung → Anwendungen“ registrieren und die Redirect-URI zeichengenau eintragen
  2. 2 Client-ID und einmalig angezeigtes Secret in der Anwendung hinterlegen
  3. 3 Als Issuer die öffentliche Adresse eintragen – die Bibliothek findet die Endpunkte über das Discovery-Dokument selbst
  4. 4 Für eigene Anwendungen „first party“ setzen, damit der Zustimmungsschritt entfällt
auth.config.ts
// 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 Host durchreichen

    Der 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_KEY sichern – getrennt von der Datenbank

    Mit 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_URL muss die echte öffentliche Adresse sein

    Sie 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 ./pgdata einplanen

    Die 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.