OpenLDAP (slapd) runs inside the phpLDAPadmin container rather than as a separate service. A custom entrypoint script renders jinja2 templates from environment variables, starts slapd in the background, bootstraps initial data if needed, then hands off to the phpLDAPadmin (Caddy) process.
This approach:
- Simplifies LDAP debugging (the cleanstart/openldap image lacks shell and system tools for interactive debugging)
- Eliminates a separate LDAP container and inter-container networking for LDAP
- Keeps slapd configuration and bootstrap data in a single Dockerfile
- Uses 127.0.0.1:389 for phpLDAPadmin → slapd communication (no network hop)
- Avoids hardcoding domain names and passwords into the image (jinja2 templates + env vars)
Base image: phpldapadmin/phpldapadmin:2.3 (Laravel-based, uses Caddy as web server)
The custom Dockerfile installs OpenLDAP server components and jinja2 on top:
FROM phpldapadmin/phpldapadmin:2.3
USER root
RUN apk add --no-cache openldap openldap-clients openldap-back-mdb openldap-overlay-memberof bash python3 py3-jinja2
RUN mkdir -p /var/lib/openldap/openldap-data /run/openldap \
&& chown -R ldap:ldap /var/lib/openldap /run/openldap
COPY files/ /
RUN chmod +x /opt/bin/*
ENTRYPOINT ["/opt/bin/entrypoint.sh"]
Key packages:
- openldap — slapd server
- openldap-clients — ldapsearch, ldapadd, etc.
- openldap-back-mdb — MDB database backend
- openldap-overlay-memberof — automatic memberOf attribute maintenance
- python3, py3-jinja2 — template rendering at container startup
phpldapadmin/
├── Dockerfile
├── variables.env # Laravel .env (legacy, being replaced by jinja2)
└── files/
├── opt/
│ ├── bin/
│ │ ├── entrypoint.sh # Custom entrypoint
│ │ └── render_variables.py # Jinja2 template renderer
│ └── etc/
│ └── openldap/
│ ├── slapd.conf.jinja2 # OpenLDAP server config template
│ ├── bootstrap.ldif.jinja2 # Initial data template
│ └── variables.env.jinja2 # Laravel .env template
└── app/
└── templates/
└── custom/
└── cicd_user.json # phpLDAPadmin user creation template
Configuration files use jinja2 templates ({{ env['VAR_NAME'] }}) rendered at container startup by render_variables.py. Environment variables come from env.txt via docker-compose env_file.
Key variables in env.txt:
- LDAP_BASE_DN — e.g. dc=pathoris,dc=de
- LDAP_ADMIN_USER — e.g. admin
- LDAP_ADMIN_PASSWORD — admin password
- LDAP_READ_USERNAME / LDAP_READ_INITIAL_PASSWORD — readonly bind account
- CICD_ADMIN_USERNAME / CICD_ADMIN_INITIAL_PASSWORD — initial CI/CD admin user
- LDAP_ORGANISATION — organization name for the base DN entry
- CI_DOMAIN — domain component for the dc: attribute
The entrypoint (/opt/bin/entrypoint.sh) performs these steps:
variables.env.jinja2 → /app/.env (phpLDAPadmin Laravel config)slapd.conf.jinja2 → /etc/openldap/slapd.conf/etc/openldap/slapd.d/ if present (cn=config takes precedence over slapd.conf)bootstrap.ldif.jinja2 → bootstrap.ldif, then runs ldapaddexecKey configuration (after template rendering):
suffix "dc=pathoris,dc=de"
rootdn "cn=admin,dc=pathoris,dc=de"
rootpw <from LDAP_ADMIN_PASSWORD>
Important: The overlay memberof directive must appear after database mdb but before suffix. This overlay automatically maintains memberOf attributes on user entries when they are added to groupOfNames groups.
readonly service account (cn=readonly,dc=pathoris,dc=de) can read all entries (used by Jenkins/Forgejo for LDAP lookups)Created on first start when the base DN doesn't exist:
dc=pathoris,dc=de)cn=admin) — also defined as rootdn in slapd.conf, sits directly under base DNcn=readonly) — used by Jenkins and Forgejo for LDAP searchesou=users — organizational unit for regular user accountscicdadmin user — initial admin account under ou=users, member of all groupsou=groups — organizational unit for groupsusers, admins, jenkins-admins, forgejo-admins (all groupOfNames)Service accounts (admin, readonly) sit directly under the base DN, not under ou=users. This is intentional — they are infrastructure accounts, not regular users.
phpLDAPadmin v2 supports custom JSON templates in /app/templates/custom/. The cicd_user.json template provides a form for creating CI/CD user accounts with:
inetOrgPerson + posixAccount objectclassesuid as the primary input field with onchange autoFill for cn, sn, mail, homeDirectorycn is the RDN, auto-filled and readonlySSHA (OpenLDAP doesn't support argon2 natively)uidNumber/gidNumber use getNextNumber auto-incrementou=users via the regexp fieldLDAP data is persisted via a volume mount in docker-compose:
volumes:
- /var/lib/cicd/ldap/data:/var/lib/openldap/openldap-data:z
Use :z (lowercase, shared SELinux label), not :Z (private). The uppercase variant can cause permission issues.
Jenkins connects to LDAP via the phpldapadmin container hostname on port 389. The 01_enable_ldap.groovy init script configures this on first boot. It uses the readonly account for searches and searches under ou=users with filter uid={0}.
Forgejo's LDAP authentication source is configured via the web UI. The User Search Base must be the full DN: ou=users,dc=pathoris,dc=de (Forgejo doesn't have a separate "Base DN" field).
podman exec -it phpldapadmin ps aux | grep slapd
podman exec -it phpldapadmin ldapsearch -x -H ldap://localhost:389 -s base -b "" namingContexts
podman exec -it phpldapadmin ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=pathoris,dc=de" -w '<LDAP_ADMIN_PASSWORD>' \
-b "dc=pathoris,dc=de" "(objectClass=*)"
podman compose stop phpldapadmin
rm -rf /var/lib/cicd/ldap/data/*
podman compose up -d --build phpldapadmin
podman exec -it phpldapadmin ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=pathoris,dc=de" -w '<LDAP_ADMIN_PASSWORD>' \
-b "ou=users,dc=pathoris,dc=de" "(uid=cicdadmin)" memberOf
| Error | Cause | Fix |
|---|---|---|
namingContexts: dc=my-domain,dc=com |
slapd.conf not rendered correctly | Check entrypoint logs, verify env vars |
Invalid credentials |
Wrong password or stale data | Reset LDAP data directory |
No such object on user search |
Search base not fully qualified | Use full DN in search base |
| phpLDAPadmin keeps restarting | slapd.conf syntax error or missing env var | Check podman logs phpldapadmin |
| Mixed content errors | Missing X-Forwarded-Proto https |
Check nginx proxy headers |
| CSRF 419 on AJAX | Wrong Referrer-Policy |
Use same-origin in nginx |