lldap mit Docker Compose: zentrales Benutzerverzeichnis für Selfhosting-Dienste
lldap ist ein schlanker LDAP-Server mit Weboberfläche für zentrale Benutzer und Gruppen. Die Anleitung zeigt Docker Compose mit v0.6.3, Benutzer per API, Leserechte-Konto, Test mit ldapsearch sowie Backup und Restore mit Schlüssel-Seed.
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Laufen im Firmennetz mehr als drei selbst gehostete Dienste, führt jeder eigene Benutzerlisten. Ein zentrales LDAP-Verzeichnis löst das, klassische Server wie OpenLDAP sind dafür aufwendig.
lldap (Light LDAP) ist ein schlanker LDAP-Server mit Weboberfläche für Benutzer, Gruppen und Passwörter. Wichtigster Fund: Mit falschem Schlüssel-Seed startet der Server nach einem Restore nicht mehr.
Was lldap ist und wo die Grenzen liegen
lldap ist ein vereinfachter LDAP-Server in Rust (GPL-3.0). Das neueste Release ist v0.6.3 vom 30.04.2026. Neben LDAP (Port 3890) gibt es eine Weboberfläche und GraphQL-API (Port 17170).
Wofür lldap gut ist:
- Zentrale Anmeldung für LDAP-fähige Dienste (Gitea, Forgejo, Nextcloud, Jellyfin, Grafana, Vaultwarden, rund 90 Beispiele in
example_configs) - Benutzerquelle für Authelia, Authentik, Keycloak oder Pocket ID (SSO per OIDC oder Forward-Auth)
- Zugriffssteuerung per
memberOf-Filter, etwa nurgit_userbei Gitea
Gewollte Grenzen:
- Kein vollständiger LDAP-Server: Über LDAP gehen nur Lesezugriffe, Benutzer anlegen und Passwörter ändern.
- Kein anonymer Bind, jeder Dienst braucht ein Bind-Konto.
- Kein Active-Directory-Ersatz (keine Gruppenrichtlinien, kein Kerberos, keine Windows-Domäne). Linux-Anmeldung per PAM und nslcd ist dokumentiert.
Voraussetzungen und Ressourcen
- Linux-Host (x86-64, ARM64 oder ARMv7, Image auch
-rootless) mit Docker Engine und Compose-Plugin, 1 CPU-Kern (Compose v2). - RAM: etwa 8,1 MiB nach dem Start, Image rund 28 MB.
- Freier Port für die Weboberfläche (LDAP nur bei externem Zugriff), produktiv Reverse Proxy mit TLS.
Welches Image-Tag: stable, v0.6.3 oder latest?
Die Doku nutzt stable, derzeit identisch mit v0.6.3. latest und datierte Tags sind Nightly-Builds. Die Anleitung pinnt v0.6.3.
| Tag | Inhalt | Empfehlung |
|---|---|---|
v0.6.3 | festes Release | Produktivbetrieb, reproduzierbar |
stable | jeweils neuestes Release | bei automatischen Updates |
latest, 2026-09-22 | Nightly aus dem Hauptzweig | nur zum Testen |
*-rootless | startet direkt als Benutzer | nach Rechteeinrichtung empfohlen |
Schritt 1: Vollständige compose.yaml und .env
Basis ist docs/install.md mit festem Tag, Bind-Mount und Geheimnissen aus einer .env.
services:
lldap:
image: lldap/lldap:v0.6.3
container_name: lldap
restart: unless-stopped
ports:
# Weboberfläche und GraphQL-API, nur lokal
- "127.0.0.1:17170:17170"
# LDAP, nur lokal
- "127.0.0.1:3890:3890"
volumes:
- "./lldap_data:/data"
environment:
- UID=1000
- GID=1000
- TZ=Europe/Berlin
- LLDAP_JWT_SECRET=${LLDAP_JWT_SECRET}
- LLDAP_KEY_SEED=${LLDAP_KEY_SEED}
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
- LLDAP_LDAP_USER_PASS=${LLDAP_LDAP_USER_PASS}
Erzeugen Sie die .env mit Zufallswerten:
mkdir -p /opt/lldap/lldap_data
cd /opt/lldap
# Zufällige Geheimnisse erzeugen
printf 'LLDAP_JWT_SECRET=%s\nLLDAP_KEY_SEED=%s\nLLDAP_LDAP_USER_PASS=%s\n' \
"$(openssl rand -base64 36 | tr -d '/+=')" \
"$(openssl rand -base64 36 | tr -d '/+=')" \
"$(openssl rand -base64 24 | tr -d '/+=')" > .env
chmod 600 .env
LLDAP_JWT_SECRET=HIER_ZUFALLSWERT_MIN_32_ZEICHEN
LLDAP_KEY_SEED=HIER_ZUFALLSWERT_MIN_12_ZEICHEN
LLDAP_LDAP_USER_PASS=HIER_START_PASSWORT_ADMIN
ls -l .env zeigt -rw-------, docker compose config zeigt alle drei Variablen mit Zufallswerten.
Dateipfade und Parameter
| Parameter oder Pfad | Bedeutung |
|---|---|
/data (Host: ./lldap_data) | users.db (SQLite mit Benutzern, Gruppen, Passwort-Hashes) und lldap_config.toml (beim ersten Start aus Vorlage angelegt). |
LLDAP_KEY_SEED | Seed für den Serverschlüssel der Passwörter, muss gleich bleiben (siehe Backup). |
LLDAP_JWT_SECRET | signiert Anmelde-Token. Ein Wechsel meldet alle Sitzungen ab. |
LLDAP_LDAP_BASE_DN | Wurzel des Verzeichnisses. |
LLDAP_LDAP_USER_PASS | Passwort von admin, gilt nur beim ersten Start. |
UID, GID | Benutzer von lldap und Eigentümer in /data (Test: 1000:1000). |
LLDAP_JWT_SECRET_FILE, LLDAP_KEY_SEED_FILE | Datei mit dem Geheimnis (Docker Secret), hat Vorrang. |
Alle Optionen (SMTP, LDAPS, LLDAP_DATABASE_URL) stehen in lldap_config.docker_template.toml. LLDAP_-Variablen überschreiben die Datei, Verschachtelung mit doppeltem Unterstrich (LLDAP_SMTP_OPTIONS__SERVER).
Schritt 2: Installation und Start
$ cd /opt/lldap
$ docker compose up -d# Startmeldungen prüfen$ docker compose logs --no-log-prefix | grep -E 'version|Starting|Could not|Successfully'
Beim ersten Start meldete lldap: Konfigurationskopie nach /data/lldap_config.toml, Starting LLDAP version 0.6.3: Migrationen bis Schema 11, die Standardgruppen und Successfully (re)set password for "admin". Die Warnung zum key_seed ist harmlos.
Das Log enthält Starting LLDAP version 0.6.3 und Successfully (re)set password for "admin", ls -ln lldap_data zeigt users.db mit Besitzer 1000.
Schritt 3: Funktionsprüfung und Healthcheck
Das Image bringt einen Healthcheck mit (/app/lldap healthcheck), in der Regel nach 30 Sekunden healthy.
$ docker compose ps# Weboberfläche: HTTP 200$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:17170/# Healthcheck manuell (Exit-Code 0)$ docker exec lldap /app/lldap healthcheck --config-file /data/lldap_config.toml
Die Ausgabe endet mit check_ldap ... Success und check_api ... Success.
docker compose ps zeigt (healthy), curl auf Port 17170 liefert 200, der manuelle Healthcheck endet mit Exit-Code 0.
Schritt 4: Erstkonfiguration: Benutzer, Gruppen und Bind-Konto
Melden Sie sich in der Weboberfläche als admin an und legen Sie Benutzer und Gruppen an. Skripte nutzen die GraphQL-API:
cd /opt/lldap
set -a; . ./.env; set +a
# Anmelde-Token holen
TOKEN=$(curl -s -X POST http://127.0.0.1:17170/auth/simple/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"admin\",\"password\":\"$LLDAP_LDAP_USER_PASS\"}" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')
# Benutzer anlegen
curl -s http://127.0.0.1:17170/api/graphql \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"mutation{createUser(user:{id:\"jdoe\",email:\"jdoe@example.com\",displayName:\"Jana Doe\",firstName:\"Jana\",lastName:\"Doe\"}){id}}"}'
# Gruppe anlegen (Antwort: Gruppen-ID)
curl -s http://127.0.0.1:17170/api/graphql \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"mutation{createGroup(name:\"git_user\"){id}}"}'
# Benutzer der Gruppe mit ID 4 zuordnen
curl -s http://127.0.0.1:17170/api/graphql \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"query":"mutation{addUserToGroup(userId:\"jdoe\",groupId:4){ok}}"}'
Passwörter setzt lldap_set_password im Container:
docker exec lldap /app/lldap_set_password \
--base-url http://localhost:17170 \
--admin-username admin --admin-password "$LLDAP_LDAP_USER_PASS" \
--username jdoe --password 'NeuesPasswort!'
Antwort: Successfully changed jdoe's password, bei falschem Admin-Passwort 401 Unauthorized.
Verbinden Sie Dienste nie mit admin, sondern mit einem Konto wie bind_user in lldap_strict_readonly, bei Passwort-Reset (etwa Authelia) mit lldap_password_manager. Mit diesem Konto lassen sich bind_user alle Benutzer lesen, das Anlegen endet mit Unauthorized user creation.
In der Weboberfläche erscheinen jdoe in der Gruppe git_user und bind_user in lldap_strict_readonly.
Schritt 5: LDAP-Anmeldung mit ldapsearch testen
Prüfen Sie mit ldapsearch (ldap-utils bzw. openldap-clients) oder einem Wegwerf-Container im Compose-Netz (<Verzeichnisname>_default, hier lldap_default):
docker run --rm --network lldap_default alpine:3.20 sh -c \
"apk add -q openldap-clients && ldapsearch -x -H ldap://lldap:3890 \
-D 'uid=bind_user,ou=people,dc=example,dc=com' -w 'BindPasswort' \
-b 'ou=people,dc=example,dc=com' '(uid=jdoe)' uid mail cn memberOf"
Die Suche lieferte uid=jdoe mit memberOf: cn=git_user,…. memberOf-Filter, member an der Gruppe und Bind als Benutzer (ldapwhoami: Success (0)) funktionierten.
Gegenproben:
- Falsches Bind-Passwort:
ldap_bind: Invalid credentials (49) - Suche ohne Bind:
Inappropriate authentication (48),Anonymous bind not allowed - Falsches Web-Passwort und GraphQL ohne Token: HTTP 401
- Benutzerliste als normaler Benutzer:
Unauthorized access to user list
ldapsearch liefert den Eintrag von jdoe mit memberOf: cn=git_user,…, ein falsches Passwort ergibt Invalid credentials (49).
Schritt 6: Praxisbeispiel: Gitea, Authelia und weitere Dienste anbinden
Vorlagen liegen im Repository. Für Gitea und Forgejo nennt example_configs/gitea.md eine Quelle LDAP (via BindDN):
- Host
lldap(Dienstname im Docker-Netz), Port3890 - Bind DN: Leserechte-Konto, etwa
uid=bind_user,ou=people,dc=example,dc=com - User Search Base:
ou=people,dc=example,dc=com - User Filter nur für Mitglieder von
git_user:(&(memberof=cn=git_user,ou=groups,dc=example,dc=com)(|(uid=%[1]s)(mail=%[1]s))) - Admin Filter:
(memberof=cn=lldap_admin,ou=groups,dc=example,dc=com); Attributeuid,givenName,sn,mail,jpegPhoto
Authelia hat die Implementierung lldap mit passenden Standardwerten, Vorlage:
authentication_backend:
refresh_interval: '1m'
ldap:
implementation: 'lldap'
address: 'ldap://lldap:3890'
base_dn: 'DC=example,DC=com'
user: 'UID=bind_user,OU=people,DC=example,DC=com'
password: 'REPLACE_ME'
Nextcloud nutzt user_ldap (Repository-Skript mit occ ldap:set-config), Pocket ID synchronisiert Benutzer und Gruppen.
Ein Mitglied von git_user kann sich am angebundenen Dienst anmelden, ein Benutzer ohne diese Gruppe nicht.
Persistente Daten und Rechte
Der Container startet als root, passt /data an und wechselt auf UID/GID. Für -rootless setzen Sie user: "1000:1000", das Verzeichnis muss diesem Benutzer gehören.
Das Admin-Passwort der .env gilt nur beim ersten Start (eine spätere Änderung wirkt nicht). Ändern Sie es in der Weboberfläche, notfalls erzwingt LLDAP_FORCE_LDAP_USER_PASS_RESET=true es beim nächsten Start (danach entfernen).
ls -ln lldap_data zeigt users.db und lldap_config.toml mit Besitzer 1000 1000.
Schritt 7: Netzwerkfreigabe, Reverse Proxy und TLS
- Port 17170 nur an
127.0.0.1binden, der Proxy leitet darauf (Traefik: Anleitung zu Forward-Auth mit Authentik und Authelia). Unter SubdomainLLDAP_HTTP_URLsetzen. - Port 3890 nur bei Zugriff von außerhalb Docker veröffentlichen, auf die interne Schnittstelle begrenzt (zum Beispiel beide Ports nur an
127.0.0.1). - LDAP ohne TLS überträgt Bind-Passwörter im Klartext. Über das Netz aktivieren Sie LDAPS mit
LLDAP_LDAPS_OPTIONS__ENABLED=true,LLDAP_LDAPS_OPTIONS__CERT_FILEundLLDAP_LDAPS_OPTIONS__KEY_FILE(Port 6360). - Geben Sie LDAP nie ins Internet frei, auch nicht mit LDAPS.
ss -ltn | grep -E '17170|3890' zeigt nur 127.0.0.1, die Weboberfläche ist über https://lldap.example.com erreichbar.
Schritt 8: Backup und Restore
Ein Backup braucht /data mit users.db und die .env (LLDAP_KEY_SEED, LLDAP_JWT_SECRET). Kaltes Backup mit kurz gestopptem Container:
$ cd /opt/lldap
$ docker compose stop lldap
$ tar czf /backup/lldap-$(date +%F).tar.gz lldap_data .env compose.yaml
$ docker compose start lldap# Archiv prüfen$ tar tzf /backup/lldap-$(date +%F).tar.gz
Prüfung des Restore: jdoe gelöscht (Bind: Invalid credentials (49)), Daten zurückgespielt. Prüfsumme der users.db identisch, Benutzer und Gruppe per ldapsearch wieder da, Bind Success (0).
$ cd /opt/lldap
$ docker compose stop lldap
$ rm -rf lldap_data
$ tar xzf /backup/lldap-2026-09-29.tar.gz lldap_data .env
$ docker compose start lldap
Mit anderem LLDAP_KEY_SEED (verlorene, neu erzeugte .env) startet lldap nicht mehr, der Container hängt in einer Neustart-Schleife:
Error: The private key has changed. ...
The private key encoding the passwords has changed since last successful startup.
Changing the private key will invalidate all existing passwords. If you want to
proceed, restart the server with the CLI arg --force-update-private-key=true or
the env variable LLDAP_FORCE_UPDATE_PRIVATE_KEY=true.
Mit dem ursprünglichen Seed lief der Datenstand sofort wieder. LLDAP_FORCE_UPDATE_PRIVATE_KEY=true macht alle Passwörter ungültig. Sichern Sie die .env getrennt geschützt mit.
Stolperstein: Nach set -a; . ./.env haben exportierte Shell-Variablen Vorrang, eine geänderte .env wirkt nicht. Neue Shell oder unset nutzen.
tar tzf listet lldap_data/users.db und .env, nach dem Restore findet ldapsearch die Benutzer wieder und der Bind liefert Success (0).
Schritt 9: Updates und Rollback-Grenzen
Update: Tag anheben, docker compose pull, docker compose up -d. Schema-Migration beim Start (bis Schema 11).
- Vor dem Update kaltes Backup ziehen und Release-Notes lesen.
- Rollback nach Schemamigration ist unzuverlässig: altes Tag und Backup zurückspielen. Kein Watchtower auf
latest(Nightly-Builds).
Das Log nennt die neue Version nach Starting LLDAP version, docker compose ps zeigt (healthy), der Bind mit ldapwhoami gelingt.
Typische Fehler mit Diagnose und Lösung
| Symptom | Ursache | Lösung |
|---|---|---|
Container startet ständig neu, Log: The private key has changed | anderer LLDAP_KEY_SEED als beim letzten Start | ursprünglichen Seed aus dem Backup der .env wiederherstellen |
Admin-Login schlägt nach Änderung in der .env fehl | Passwort aus der Konfiguration gilt nur beim Erststart | altes Passwort nutzen oder einmalig LLDAP_FORCE_LDAP_USER_PASS_RESET=true |
Inappropriate authentication (48), Anonymous bind not allowed | Dienst versucht anonymen Bind | Bind DN und Passwort eines Leserechte-Kontos eintragen |
Invalid credentials (49) | falsches Passwort, falscher Bind DN, Benutzer fehlt | DN uid=NAME,ou=people,dc=... und Base DN prüfen |
Can't contact LDAP server (-1) | lldap läuft nicht oder Dienst ist nicht im selben Docker-Netz | docker compose ps: Netz und Log prüfen |
lldap_set_password meldet 401 Unauthorized | falsches oder leeres Admin-Passwort | Variable in der Shell prüfen, Passwort in Anführungszeichen |
| Dienst zeigt keine Gruppen | falscher Gruppenfilter oder falsches Mitgliedsattribut | Gruppen unter ou=groups: Attribut member: Benutzer tragen memberOf |
Keine users.db in /data | Verzeichnis für den Container-Benutzer nicht beschreibbar | Eigentümer auf UID:GID setzen |
Mit verbose = true in der lldap_config.toml zeigt das Log jede LDAP-Anfrage samt Attributen.
Saubere Deinstallation
Achtung, Datenverlust: Die Befehle löschen alle Benutzer, Gruppen und Passwort-Hashes unwiderruflich, angebundene Dienste verlieren ihre Anmeldequelle. Ziehen Sie vorher ein Backup.
$ cd /opt/lldap
$ docker compose down -v --remove-orphans
$ docker rmi lldap/lldap:v0.6.3# Daten und Geheimnisse entfernen$ rm -rf /opt/lldap
Gehören Dateien in lldap_data einem anderen Benutzer, löschen Sie mit sudo.
Häufige Fragen
Was passiert, wenn der LLDAP_KEY_SEED verloren geht?
Mit einem anderen Seed startet lldap nicht mehr, der Container hängt mit The private key has changed in einer Neustart-Schleife. LLDAP_FORCE_UPDATE_PRIVATE_KEY=true macht alle Passwörter ungültig. Sichern Sie die .env deshalb immer getrennt geschützt mit.
Warum wirkt ein geändertes Admin-Passwort in der .env nicht?
Das Passwort aus der .env gilt nur beim ersten Start. Ändern Sie es in der Weboberfläche, notfalls erzwingt LLDAP_FORCE_LDAP_USER_PASS_RESET=true es beim nächsten Start (danach wieder entfernen).
Mit welchem Konto binden sich Dienste an?
Nie mit admin, sondern mit einem Konto wie bind_user in der Gruppe lldap_strict_readonly. Braucht ein Dienst den Passwort-Reset, etwa Authelia, kommt das Konto in lldap_password_manager.
Ersetzt lldap ein Active Directory?
Nein. lldap bietet keine Gruppenrichtlinien, kein Kerberos und keine Windows-Domäne. Über LDAP gehen nur Lesezugriffe, Benutzer anlegen und Passwörter ändern.
Welches Image-Tag eignet sich für den Betrieb?
Ein festes Release wie v0.6.3. latest und datierte Tags sind Nightly-Builds aus dem Hauptzweig, deshalb kein Watchtower auf latest.
Fazit
lldap bündelt Benutzer und Gruppen für LDAP-fähige Dienste in einem schlanken Container mit Weboberfläche, ohne den Aufwand eines klassischen LDAP-Servers. Binden Sie beide Ports an 127.0.0.1, verbinden Sie Dienste nur über ein Leserechte-Konto und aktivieren Sie LDAPS, sobald Bind-Passwörter über das Netz gehen. Sichern Sie lldap_data immer zusammen mit der .env, denn ohne den ursprünglichen LLDAP_KEY_SEED startet der Server nicht mehr.
Weiterführende Anleitungen und Quellen
- SSO mit Authentik oder Authelia und Traefik Forward-Auth
- Pocket ID mit Docker Compose
- Gitea als eigener Git-Server mit Docker
Grundlagen dazu: Docker und Docker Compose auf Linux installieren, Docker Compose absichern, Docker-Container automatisch aktualisieren.
Quellen
- lldap auf GitHub (README, Architektur-Empfehlung, Berechtigungsgruppen)
- lldap: Installationsanleitung mit Docker-Compose-Beispiel
- lldap: Konfigurationsvorlage mit allen Optionen
- lldap: Skripting über LDAP und GraphQL
- lldap: FAQ zu Anmeldeproblemen
- lldap: Beispielkonfigurationen für Gitea, Authelia, Nextcloud, Pocket ID und weitere Dienste
- lldap Release v0.6.3 vom 30.04.2026
- Docker Hub: lldap/lldap mit Tags und Architekturen
- Authelia: Integrationsleitfaden für lldap


