OpenLDAP LTS and feature images on Alpine and Debian with env bootstrap and optional slapd.d mode.
10K+
This project builds a complete OpenLDAP image on Alpine Linux, compiles a pinned OpenLDAP version directly from source, and includes dynamic backends, overlays, and password modules directly in the image. By default, the container seeds and starts from a generated slapd.conf, but it can also switch to a persistent slapd.d / cn=config runtime mode. The configuration follows the style of well-known container images: as much as practical via environment variables, everything else via LDIF bootstrap, schema directories, and config snippets.
slapd.d Modeslapd.d / cn=configmdbdocker-entrypoint-initdb.d.schema files and .conf snippetsstdout/stderrldapi.
├── Dockerfile
├── docker-entrypoint.sh
├── docker-healthcheck.sh
├── README.md
├── .env.example
├── .gitignore
├── Makefile
└── examples
├── docker-compose.yml
├── bootstrap
│ └── 20-demo-user.ldif
└── custom-config
├── post
│ └── 70-extra.conf
└── pre
└── 50-global.conf
The image is built in two stages:
su-execThe OpenLDAP build enables dynamic modules so the image can load not only mdb, but also additional backends and overlays from the selected upstream release. The 2.6 LTS channel includes back-sql and its ODBC runtime; OpenLDAP removed back-sql and back-perl from 2.7, so the stable channel intentionally does not contain them. The build also compiles the upstream pw-sha2 contrib password module into the image. The generated default configuration intentionally focuses on a clean mdb setup. For special cases, you can load additional modules, provide your own slapd.conf, or persist slapd.d as the runtime source of truth.
make build CHANNEL=lts TAG=2.6-lts
make build CHANNEL=stable TAG=2.7-stable
The default channel is lts. A direct docker build without build arguments therefore also builds the current LTS pin. To build the exact checked-in pins explicitly (select either channel manifest):
. versions/alpine.env
. versions/openldap-lts.env
docker build \
--build-arg ALPINE_VERSION="$ALPINE_VERSION" \
--build-arg ALPINE_DIGEST="$ALPINE_DIGEST" \
--build-arg OPENLDAP_CHANNEL="$OPENLDAP_CHANNEL" \
--build-arg OPENLDAP_VERSION="$OPENLDAP_VERSION" \
--build-arg OPENLDAP_SHA256="$OPENLDAP_SHA256" \
--build-arg IMAGE_REVISION="$IMAGE_REVISION" \
-t "openldap:${OPENLDAP_VERSION}-r${IMAGE_REVISION}" .
Multi-arch build with buildx:
. versions/alpine.env
. versions/openldap-lts.env
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg ALPINE_VERSION="$ALPINE_VERSION" \
--build-arg ALPINE_DIGEST="$ALPINE_DIGEST" \
--build-arg OPENLDAP_CHANNEL="$OPENLDAP_CHANNEL" \
--build-arg OPENLDAP_VERSION="$OPENLDAP_VERSION" \
--build-arg OPENLDAP_SHA256="$OPENLDAP_SHA256" \
--build-arg IMAGE_REVISION="$IMAGE_REVISION" \
-t "openldap:${OPENLDAP_VERSION}-r${IMAGE_REVISION}" \
.
Publish to your own Docker Hub namespace:
docker login
make push CHANNEL=lts IMAGE_NAME=<your-namespace>/openldap TAG=2.6-lts
make push CHANNEL=stable IMAGE_NAME=<your-namespace>/openldap TAG=2.7-stable
Create a local SBOM export:
make sbom-local
docker run -d \
--name openldap \
-p 389:389 \
-p 636:636 \
-e LDAP_DOMAIN=example.org \
-e LDAP_BASE_DN=dc=example,dc=org \
-e LDAP_ORGANISATION="Example Inc." \
-e LDAP_ADMIN_PASSWORD=supersecret \
-v $(pwd)/data/openldap:/var/lib/openldap/openldap-data \
-v $(pwd)/data/accesslog:/var/lib/openldap/accesslog \
-v $(pwd)/examples/bootstrap:/docker-entrypoint-initdb.d:ro \
openldap
ldapsearch -x -H ldap://127.0.0.1:389 -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w supersecret
This repository includes a GitHub Actions workflow at .github/workflows/docker-publish.yml that publishes the maintainer image to Docker Hub as chrroessner/openldap.
It also includes .github/workflows/openldap-upstream-check.yml, which runs daily and tracks three upstream streams independently:
Each stream uses its own pull-request branch (automation/openldap-lts, automation/openldap-stable, automation/alpine-stable, and automation/debian-stable). OpenLDAP updates never rewrite this README: stable changes only its own manifest; LTS also synchronizes the default build arguments in Dockerfile, Dockerfile.debian, and examples/docker-compose.yml. This keeps the two OpenLDAP PRs mergeable in either order. The linked manifests are the source of truth for current versions, checksums, and image revisions.
After a pin change reaches main or master, the upstream check regenerates outstanding update PRs against that base. This also refreshes Alpine PRs, which intentionally touch both channel revisions and can overlap with OpenLDAP updates. The check can also be run manually to refresh an existing PR.
Every pull request runs .github/workflows/ci.yml: it verifies the release/tag contract, builds both channels in both variants (Alpine and Debian), checks their labels and modules, and performs an isolated LDAPI health smoke test. The contract test explicitly rejects latest in the stable channel.
If you fork this repository, adjust the workflow image name and use your own Docker Hub namespace. The example push commands in this README intentionally use <your-namespace>/openldap for that reason.
The publish workflow runs:
mainmasterv*workflow_dispatchThe daily upstream check compares both the exact Alpine stable patch version and the official Docker Hub multiarch digest. An Alpine patch update or a replacement digest for the same patch release creates one separate automation/alpine-stable PR. That PR pins the new base image and increments IMAGE_REVISION in both OpenLDAP channel manifests. Merging it therefore publishes a new -rX image for LTS and stable; there is no silent scheduled rebuild under an existing revision. Before publishing, the workflow checks Docker Hub and never overwrites an existing -rX tag.
Published tag policy:
| Channel | Moving tags | Exact tag | Compatibility |
|---|---|---|---|
| OpenLDAP 2.6 LTS | latest, lts, 2.6, 2.6-lts | <lts-version>, <lts-version>-r<revision> | Existing 2.6 MDB volumes remain on the LTS line |
| OpenLDAP 2.7 feature/stable | stable, 2.7, 2.7-stable | <stable-version>, <stable-version>-r<revision> | Requires the documented 2.6-to-2.7 MDB export/import |
Both channels also publish an exact Alpine-qualified tag of the form <openldap-version>-alpine<alpine-version>. The -rX tag identifies the channel-specific image revision: OpenLDAP version changes reset it to r1, while an Alpine version/digest update or a refreshed OpenLDAP source checksum increments it. latest deliberately remains on LTS because an automatic move from 2.6 to 2.7 would make existing MDB volumes unusable until migrated.
Fixes that upstream has merged but not yet released live in patches/<openldap-version>/ and are applied in name
order with patch -p1 --forward right after the source checksum check, in both Dockerfiles. They apply only to that
exact OpenLDAP version: a version bump skips them, so remove the directory once the release contains the fix, and a
patch that stops applying to its version fails the build. Adding or changing a patch increments IMAGE_REVISION of the
affected channel.
| Version | Patch | Reason |
|---|---|---|
| 2.6.15, 2.7.1 | 0001-ITS-10597-slapo-accesslog-per-op-state.patch (upstream 1d1db8682) | ITS#10597: slapo-accesslog freed shared per-operation state without its mutex, so concurrent non-write operations such as binds and searches with logops writes and logsuccess TRUE could double-free and crash slapd. Affects 2.6.14, 2.6.15, 2.7.0 and 2.7.1; targeted for 2.6.16. |
Each channel is additionally published as a Debian-based image, built from the same pinned OpenLDAP source with the same configure flags and modules. It differs only in the C library: glibc instead of musl. Choose it when a workload is sensitive to musl's allocator, for example under heavy concurrent load: musl's malloc serializes on one global lock and stops the process as soon as it detects heap corruption, where glibc may not notice it. Thread stacks are not a reason: slapd sets an explicit 8 MiB stack for its own threads on both variants.
| Channel | Moving tags | Exact tags |
|---|---|---|
| OpenLDAP 2.6 LTS | lts-debian, 2.6-debian, 2.6-lts-debian | <lts-version>-debian, <lts-version>-r<revision>-debian, <lts-version>-debian<debian-version> |
| OpenLDAP 2.7 feature/stable | stable-debian, 2.7-debian, 2.7-stable-debian | <stable-version>-debian, <stable-version>-r<revision>-debian, <stable-version>-debian<debian-version> |
latest; latest stays the Alpine LTS image.ldap user keeps UID 100 and GID 101, exactly as in the Alpine image, so both variants run on the same persistent volumes without an ownership change.su-exec package, so su-exec is a symlink to gosu, which takes the same user:group command arguments.versions/debian.env (version and multiarch digest). The -rX revision is shared per channel with the Alpine image.automation/debian-stable PR that pins the new base and increments both channel revisions. A new Debian major version is deliberately left to a manual change, because it changes package names.Build it locally with make build VARIANT=debian (optionally CHANNEL=stable).
SBOM is integrated in three places:
linux/amd64 and linux/arm64Required GitHub repository secrets:
DOCKERHUB_USERNAMEDOCKERHUB_TOKENRecommended Docker Hub setup:
<your-namespace>/openldaplatest on the LTS channelstable for the current OpenLDAP feature releasev<openldap-version>-r<revision>; the requested revision must match the pinned channel revisionLocal SBOM usage:
make sbom-local
ls dist/sbom-local | grep sbom
Registry SBOM inspection after push:
make sbom-registry IMAGE_NAME=<your-namespace>/openldap TAG=latest
Suggested first publish:
. versions/openldap-lts.env
git tag "v${OPENLDAP_VERSION}-r${IMAGE_REVISION}"
git push origin main --tags
The repository content is licensed under the MIT License. See LICENSE.
The published container image additionally includes OpenLDAP, which is distributed under OLDAP-2.8. Because of that, the OCI image metadata declares MIT AND OLDAP-2.8.
On startup, the following happens:
LDAP_CONFIG_BACKEND=slapd.conf, it validates and starts from slapd.conf.LDAP_CONFIG_BACKEND=slapd.d, the container behaves in one of two modes:
LDAP_CONFIG_DIR is empty, it seeds slapd.d once from slapd.conf using slaptest -f ... -F ...LDAP_CONFIG_DIR already contains data, the persisted slapd.d tree is treated as authoritativeslapd.conf, a fresh directory is initialized:
LDAP_BASE_DNpeople and groups OUsdocker-entrypoint-initdb.dslapd.d tree is reused, env-driven bootstrap is skipped intentionally because the live cn=config tree may already differ from the current environment values.slapd starts in the foreground and logs to the container output.| Path in Container | Purpose |
|---|---|
/var/lib/openldap/openldap-data | Primary mdb database |
/var/lib/openldap/accesslog | Separate accesslog DB |
/docker-entrypoint-initdb.d | First-time initialization (.ldif, .sh) |
/etc/openldap/custom-schema | Additional .schema files |
/etc/openldap/custom-config/pre | Additional global config before DB blocks |
/etc/openldap/custom-config/post | Additional config after the generated DB blocks |
/etc/openldap/certs | Certificates/keys for TLS |
| Variable | Default | Meaning |
|---|---|---|
LDAP_DOMAIN | empty | DNS domain as convenience input |
LDAP_BASE_DN | dc=example,dc=org | Base DN / suffix |
LDAP_ORGANISATION | derived from base DN | Value for the base entry |
LDAP_ADMIN_USERNAME | admin | CN of the default admin |
LDAP_ADMIN_PASSWORD | – | Admin password in plain text |
LDAP_ADMIN_PASSWORD_FILE | – | Password from file/secret |
LDAP_ADMIN_PASSWORD_HASH | – | Pre-hashed alternative to LDAP_ADMIN_PASSWORD for runtime authentication; first-start LDAP bootstrap still needs the plain password |
LDAP_PASSWORD_HASH_SCHEME | {ARGON2} | Scheme used when the container derives LDAP_ADMIN_PASSWORD_HASH from LDAP_ADMIN_PASSWORD |
LDAP_ADMIN_DN | cn=<admin>,<baseDN> | Full DN of the admin |
| Variable | Default | Meaning |
|---|---|---|
LDAP_ENABLE_LDAP | true | Enable LDAP on port 389 |
LDAP_ENABLE_LDAPS | same as LDAP_ENABLE_TLS | Enable LDAPS on port 636 |
LDAP_PORT_NUMBER | 389 | LDAP port |
LDAP_LDAPS_PORT_NUMBER | 636 | LDAPS port |
LDAP_LDAPI_URI | ldapi://%2Fvar%2Frun%2Fopenldap%2Fldapi | Local IPC socket used for health checks and bootstrap |
LDAP_LOG_LEVEL | 256 | slapd.conf loglevel |
LDAP_DEBUG_LEVEL | same as LDAP_LOG_LEVEL | Numeric slapd -d value |
LDAP_THREADS | empty | Optional thread tuning |
LDAP_TIMELIMIT | empty | Optional global search limit |
LDAP_SIZELIMIT | empty | Optional global size limit |
| Variable | Default | Meaning |
|---|---|---|
LDAP_DB_DIR | /var/lib/openldap/openldap-data | Data path of the main DB |
LDAP_MDB_MAXSIZE | 1073741824 | mdb maxsize |
LDAP_MDB_CHECKPOINT | 1024 5 | checkpoint for mdb |
LDAP_MDB_DBNOSYNC | false | Optional dbnosync |
LDAP_SKIP_DEFAULT_TREE | false | Do not create the base tree automatically |
LDAP_CREATE_PEOPLE_OU | true | Create ou=people |
LDAP_CREATE_GROUPS_OU | true | Create ou=groups |
LDAP_PEOPLE_OU | people | Name of the user OU |
LDAP_GROUPS_OU | groups | Name of the group OU |
LDAP_INITDB_DIR | /docker-entrypoint-initdb.d | Init directory |
| Variable | Default | Meaning |
|---|---|---|
LDAP_CONFIG_BACKEND | slapd.conf | Runtime config backend: slapd.conf or slapd.d |
LDAP_CONFIG_DIR | /etc/openldap/slapd.d | Persistent slapd.d directory |
LDAP_EXTRA_SCHEMAS | cosine inetorgperson nis | Additional standard schemas |
LDAP_LOAD_MODULES | empty | Additional modules, comma- or space-separated |
LDAP_CUSTOM_SCHEMA_DIR | /etc/openldap/custom-schema | Custom .schema files |
LDAP_CUSTOM_PRECONFIG_DIR | /etc/openldap/custom-config/pre | Additional global config |
LDAP_CUSTOM_POSTCONFIG_DIR | /etc/openldap/custom-config/post | Additional DB/overlay config |
LDAP_SKIP_DEFAULT_CONFIG | false | Use your own complete slapd.conf |
| Variable | Default | Meaning |
|---|---|---|
LDAP_ENABLE_TLS | false | Enable TLS directives in slapd.conf |
LDAP_REQUIRE_TLS | false | Block unencrypted simple binds |
LDAP_TLS_CERT_FILE | empty | Server certificate |
LDAP_TLS_KEY_FILE | empty | Private key |
LDAP_TLS_CA_FILE | empty | CA file |
LDAP_TLS_DH_PARAM_FILE | empty | Optional DH parameters |
LDAP_TLS_CIPHER_SUITE | empty | Optional cipher suite |
LDAP_TLS_VERIFY_CLIENT | never | TLSVerifyClient |
LDAP_SIMPLE_BIND_MIN_SSF | 128 | Minimum SSF when LDAP_REQUIRE_TLS=true |
| Variable | Default | Meaning |
|---|---|---|
LDAP_ENABLE_MONITOR_DB | true | Add database monitor |
LDAP_ENABLE_SYNCPROV | false | Enable overlay syncprov |
LDAP_SYNCPROV_CHECKPOINT | 100 10 | syncprov-checkpoint |
LDAP_SYNCPROV_SESSIONLOG | empty | Optional syncprov-sessionlog |
LDAP_ENABLE_MEMBEROF | false | Enable memberOf overlay |
LDAP_ENABLE_REFINT | same as LDAP_ENABLE_MEMBEROF | Enable refint overlay |
LDAP_ENABLE_ACCESSLOG | false | Enable accesslog DB + overlay |
LDAP_ACCESSLOG_SUFFIX | cn=accesslog | Suffix of the accesslog DB |
LDAP_ACCESSLOG_ROOTDN | cn=accesslog | RootDN of the accesslog DB |
LDAP_ACCESSLOG_DB_DIR | /var/lib/openldap/accesslog | Path of the accesslog DB |
LDAP_ACCESSLOG_MAXSIZE | 268435456 | mdb maxsize for accesslog |
LDAP_ACCESSLOG_LOGOPS | writes | logops |
LDAP_ACCESSLOG_LOGPURGE | 07+00:00 01+00:00 | logpurge |
For upstream-contributed modules, shipped .schema and .ldif files are copied into the image schema directory during the build when they exist. The pw-sha2 password module is built and installed as pw-sha2.la / pw-sha2.so under the OpenLDAP module directory, so custom configurations can load it with moduleload pw-sha2.la or moduleload pw-sha2.so. The accesslog overlay is a special case: OpenLDAP 2.6 registers its audit schema from the module itself, so there is no separate upstream audit.schema file to install.
Example:
docker run -d \
--name openldap \
-p 389:389 \
-p 636:636 \
-e LDAP_DOMAIN=example.org \
-e LDAP_BASE_DN=dc=example,dc=org \
-e LDAP_ADMIN_PASSWORD=supersecret \
-e LDAP_ENABLE_TLS=true \
-e LDAP_ENABLE_LDAPS=true \
-e LDAP_REQUIRE_TLS=true \
-e LDAP_TLS_CERT_FILE=/etc/openldap/certs/tls.crt \
-e LDAP_TLS_KEY_FILE=/etc/openldap/certs/tls.key \
-e LDAP_TLS_CA_FILE=/etc/openldap/certs/ca.crt \
-v $(pwd)/certs:/etc/openldap/certs:ro \
openldap
The /docker-entrypoint-initdb.d directory is processed only when initializing an empty database for the first time.
Supported file types:
*.ldif
changetype: -> ldapmodifychangetype: -> ldapadd*.sh
/bin/shAll LDIFs are applied locally via ldapi:/// during bootstrap. When runtime policy requires transport security, choose a LDAP_SIMPLE_BIND_MIN_SSF value that still permits the local bootstrap path while continuing to reject insecure remote simple binds.
Example file: examples/bootstrap/20-demo-user.ldif
Place .schema files in:
/etc/openldap/custom-schema
These files are automatically included via include.
Before the database blocks:
/etc/openldap/custom-config/pre
After the generated database blocks:
/etc/openldap/custom-config/post
This allows you to add, for example:
security, limits, threadsIf you want to bypass the default generation completely:
slapd.confLDAP_SKIP_DEFAULT_CONFIG=trueExample:
docker run -d \
--name openldap \
-e LDAP_SKIP_DEFAULT_CONFIG=true \
-v $(pwd)/my-slapd.conf:/etc/openldap/slapd.conf:ro \
-v $(pwd)/data:/var/lib/openldap/openldap-data \
openldap
In this mode, you are fully responsible for the configuration.
slapd.d ModeIf you prefer cn=config / slapd.d at runtime:
docker run -d \
--name openldap \
-e LDAP_CONFIG_BACKEND=slapd.d \
-v $(pwd)/slapd.d:/etc/openldap/slapd.d \
-v $(pwd)/data:/var/lib/openldap/openldap-data \
openldap
Behavior in this mode:
LDAP_CONFIG_DIR is seeded from the current slapd.confLDAP_CONFIG_DIR already contains data, the persisted slapd.d tree is used as-isslapd.d exists, environment-based config changes are no longer reapplied automaticallycn=config tree becomes the source of truthThe health check runs against:
ldapi://%2Fvar%2Frun%2Fopenldap%2Fldapi
It uses:
ldapsearch -Q -Y EXTERNAL -H ldapi://%2Fvar%2Frun%2Fopenldap%2Fldapi -LLL -s base -b "" namingContexts
This means container health does not depend on externally reachable ports or admin credentials.
slapd.conf, but both slapd.conf and persistent slapd.d runtime modes are supported.mdb. Other backends are available in the image, but should be configured through your own snippets or a custom slapd.conf.slapcat and imported with the 2.7 slapadd; never point a 2.7 container directly at a 2.6 data volume.back-sql and back-perl. Audit custom LDAP_LOAD_MODULES, slapd.conf, and persisted slapd.d trees before changing from the LTS to the stable channel.LDAP_CONFIG_BACKEND=slapd.d, a populated LDAP_CONFIG_DIR becomes authoritative. Environment-based config changes are then seed-only and are nContent type
Image
Digest
sha256:8d9d4ac67…
Size
14 MB
Last updated
6 days ago
docker pull chrroessner/openldap