Sign inSign up

chrroessner/openldap

By chrroessner

•Updated 6 days ago

OpenLDAP LTS and feature images on Alpine and Debian with env bootstrap and optional slapd.d mode.

Image
0

10K+

chrroessner/openldap repository overview

⁠OpenLDAP on Alpine

Publish Docker Image License: MIT Last Commit Stars

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.

⁠Table of Contents

⁠Goals

  • Alpine-based runtime
  • Pinned OpenLDAP source version via Docker build args
  • Env-driven default configuration with optional persistent slapd.d / cn=config
  • Complete image including compiled backends, overlays, and password modules
  • Solid defaults for mdb
  • First-time initialization via docker-entrypoint-initdb.d
  • TLS, accesslog, syncprov, memberOf/refint can be enabled via env vars
  • Extensible through custom .schema files and .conf snippets
  • Clean container logs via stdout/stderr
  • Health check via local ldapi

⁠Project Structure

.
├── 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

⁠Included Components

The image is built in two stages:

  • A builder stage compiles OpenLDAP from the official source tarball
  • A runtime stage keeps Alpine as the base image and only adds the required runtime libraries plus su-exec

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

⁠Quick Start

⁠1. Build
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
⁠2. Start the container
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
⁠3. Test
ldapsearch -x -H ldap://127.0.0.1:389 -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w supersecret

⁠Publishing

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:

  • on pushes to main
  • on pushes to master
  • on Git tags matching v*
  • manually via workflow_dispatch

The 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:

ChannelMoving tagsExact tagCompatibility
OpenLDAP 2.6 LTSlatest, 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/stablestable, 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.

⁠Upstream patches

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.

VersionPatchReason
2.6.15, 2.7.10001-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.
⁠Debian variant

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.

ChannelMoving tagsExact tags
OpenLDAP 2.6 LTSlts-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/stablestable-debian, 2.7-debian, 2.7-stable-debian<stable-version>-debian, <stable-version>-r<revision>-debian, <stable-version>-debian<debian-version>
  • The Debian variant never publishes latest; latest stays the Alpine LTS image.
  • The 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.
  • The entrypoint is shared. Debian has no su-exec package, so su-exec is a symlink to gosu, which takes the same user:group command arguments.
  • The base image is pinned in versions/debian.env (version and multiarch digest). The -rX revision is shared per channel with the Alpine image.
  • The daily upstream check tracks the Debian base like the Alpine one: a new point release or a replacement digest within the pinned major version opens an 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:

  • the published image gets OCI attestations for both provenance and SBOM
  • the workflow exports downloadable SPDX JSON files for linux/amd64 and linux/arm64
  • the Dockerfile enables BuildKit SBOM scanning for both the build context and the builder stage, so the SBOM is not limited to the final runtime layer

Required GitHub repository secrets:

  • DOCKERHUB_USERNAME
  • DOCKERHUB_TOKEN

Recommended Docker Hub setup:

  • create a public repository in your own namespace, for example <your-namespace>/openldap
  • create a Docker Hub access token dedicated to CI
  • keep latest on the LTS channel
  • use stable for the current OpenLDAP feature release
  • repository release tags may use v<openldap-version>-r<revision>; the requested revision must match the pinned channel revision

Local 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

⁠License

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.

⁠Operating Model

On startup, the following happens:

  1. The entrypoint script reads and normalizes the environment variables.
  2. If LDAP_CONFIG_BACKEND=slapd.conf, it validates and starts from slapd.conf.
  3. If LDAP_CONFIG_BACKEND=slapd.d, the container behaves in one of two modes:
    • If LDAP_CONFIG_DIR is empty, it seeds slapd.d once from slapd.conf using slaptest -f ... -F ...
    • If LDAP_CONFIG_DIR already contains data, the persisted slapd.d tree is treated as authoritative
  4. If the data directory is empty and the active configuration was seeded from the current env-driven slapd.conf, a fresh directory is initialized:
    • Base entry for LDAP_BASE_DN
    • Optional admin entry
    • Optional people and groups OUs
    • Followed by processing docker-entrypoint-initdb.d
  5. If a persisted slapd.d tree is reused, env-driven bootstrap is skipped intentionally because the live cn=config tree may already differ from the current environment values.
  6. After that, slapd starts in the foreground and logs to the container output.

⁠Important Volumes / Mountpoints

Path in ContainerPurpose
/var/lib/openldap/openldap-dataPrimary mdb database
/var/lib/openldap/accesslogSeparate accesslog DB
/docker-entrypoint-initdb.dFirst-time initialization (.ldif, .sh)
/etc/openldap/custom-schemaAdditional .schema files
/etc/openldap/custom-config/preAdditional global config before DB blocks
/etc/openldap/custom-config/postAdditional config after the generated DB blocks
/etc/openldap/certsCertificates/keys for TLS

⁠Environment Variables

⁠Core Parameters
VariableDefaultMeaning
LDAP_DOMAINemptyDNS domain as convenience input
LDAP_BASE_DNdc=example,dc=orgBase DN / suffix
LDAP_ORGANISATIONderived from base DNValue for the base entry
LDAP_ADMIN_USERNAMEadminCN 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_DNcn=<admin>,<baseDN>Full DN of the admin
⁠Listener / Runtime
VariableDefaultMeaning
LDAP_ENABLE_LDAPtrueEnable LDAP on port 389
LDAP_ENABLE_LDAPSsame as LDAP_ENABLE_TLSEnable LDAPS on port 636
LDAP_PORT_NUMBER389LDAP port
LDAP_LDAPS_PORT_NUMBER636LDAPS port
LDAP_LDAPI_URIldapi://%2Fvar%2Frun%2Fopenldap%2FldapiLocal IPC socket used for health checks and bootstrap
LDAP_LOG_LEVEL256slapd.conf loglevel
LDAP_DEBUG_LEVELsame as LDAP_LOG_LEVELNumeric slapd -d value
LDAP_THREADSemptyOptional thread tuning
LDAP_TIMELIMITemptyOptional global search limit
LDAP_SIZELIMITemptyOptional global size limit
⁠Database / Bootstrap
VariableDefaultMeaning
LDAP_DB_DIR/var/lib/openldap/openldap-dataData path of the main DB
LDAP_MDB_MAXSIZE1073741824mdb maxsize
LDAP_MDB_CHECKPOINT1024 5checkpoint for mdb
LDAP_MDB_DBNOSYNCfalseOptional dbnosync
LDAP_SKIP_DEFAULT_TREEfalseDo not create the base tree automatically
LDAP_CREATE_PEOPLE_OUtrueCreate ou=people
LDAP_CREATE_GROUPS_OUtrueCreate ou=groups
LDAP_PEOPLE_OUpeopleName of the user OU
LDAP_GROUPS_OUgroupsName of the group OU
LDAP_INITDB_DIR/docker-entrypoint-initdb.dInit directory
⁠Schemas / Modules / Extension
VariableDefaultMeaning
LDAP_CONFIG_BACKENDslapd.confRuntime config backend: slapd.conf or slapd.d
LDAP_CONFIG_DIR/etc/openldap/slapd.dPersistent slapd.d directory
LDAP_EXTRA_SCHEMAScosine inetorgperson nisAdditional standard schemas
LDAP_LOAD_MODULESemptyAdditional modules, comma- or space-separated
LDAP_CUSTOM_SCHEMA_DIR/etc/openldap/custom-schemaCustom .schema files
LDAP_CUSTOM_PRECONFIG_DIR/etc/openldap/custom-config/preAdditional global config
LDAP_CUSTOM_POSTCONFIG_DIR/etc/openldap/custom-config/postAdditional DB/overlay config
LDAP_SKIP_DEFAULT_CONFIGfalseUse your own complete slapd.conf
⁠TLS
VariableDefaultMeaning
LDAP_ENABLE_TLSfalseEnable TLS directives in slapd.conf
LDAP_REQUIRE_TLSfalseBlock unencrypted simple binds
LDAP_TLS_CERT_FILEemptyServer certificate
LDAP_TLS_KEY_FILEemptyPrivate key
LDAP_TLS_CA_FILEemptyCA file
LDAP_TLS_DH_PARAM_FILEemptyOptional DH parameters
LDAP_TLS_CIPHER_SUITEemptyOptional cipher suite
LDAP_TLS_VERIFY_CLIENTneverTLSVerifyClient
LDAP_SIMPLE_BIND_MIN_SSF128Minimum SSF when LDAP_REQUIRE_TLS=true
⁠Overlays / Extra Features
VariableDefaultMeaning
LDAP_ENABLE_MONITOR_DBtrueAdd database monitor
LDAP_ENABLE_SYNCPROVfalseEnable overlay syncprov
LDAP_SYNCPROV_CHECKPOINT100 10syncprov-checkpoint
LDAP_SYNCPROV_SESSIONLOGemptyOptional syncprov-sessionlog
LDAP_ENABLE_MEMBEROFfalseEnable memberOf overlay
LDAP_ENABLE_REFINTsame as LDAP_ENABLE_MEMBEROFEnable refint overlay
LDAP_ENABLE_ACCESSLOGfalseEnable accesslog DB + overlay
LDAP_ACCESSLOG_SUFFIXcn=accesslogSuffix of the accesslog DB
LDAP_ACCESSLOG_ROOTDNcn=accesslogRootDN of the accesslog DB
LDAP_ACCESSLOG_DB_DIR/var/lib/openldap/accesslogPath of the accesslog DB
LDAP_ACCESSLOG_MAXSIZE268435456mdb maxsize for accesslog
LDAP_ACCESSLOG_LOGOPSwriteslogops
LDAP_ACCESSLOG_LOGPURGE07+00:00 01+00:00logpurge

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.

⁠Enabling TLS

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

⁠Init Scripts and LDIFs

The /docker-entrypoint-initdb.d directory is processed only when initializing an empty database for the first time.

Supported file types:

  • *.ldif
    • with changetype: -> ldapmodify
    • without changetype: -> ldapadd
  • *.sh
    • executable or run via /bin/sh

All 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

⁠Custom Schemas

Place .schema files in:

/etc/openldap/custom-schema

These files are automatically included via include.

⁠Custom Config Snippets

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:

  • Global security, limits, threads
  • Additional databases
  • More overlays
  • Fine-grained ACL adjustments
  • Experimental or rare modules

⁠Fully Custom Configuration

If you want to bypass the default generation completely:

  1. Mount your own slapd.conf
  2. Set LDAP_SKIP_DEFAULT_CONFIG=true

Example:

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.

⁠One-Shot slapd.d Mode

If 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:

  • On the first start, an empty LDAP_CONFIG_DIR is seeded from the current slapd.conf
  • On later starts, if LDAP_CONFIG_DIR already contains data, the persisted slapd.d tree is used as-is
  • After slapd.d exists, environment-based config changes are no longer reapplied automatically
  • This is intentional: the persisted cn=config tree becomes the source of truth

⁠Health Check

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

⁠Notes and Limitations

  • The default env-driven bootstrap path seeds from slapd.conf, but both slapd.conf and persistent slapd.d runtime modes are supported.
  • OpenLDAP is built from source in the builder stage; Alpine provides the runtime base image and shared runtime libraries.
  • The default generation is tailored to mdb. Other backends are available in the image, but should be configured through your own snippets or a custom slapd.conf.
  • OpenLDAP 2.7 uses a newer LMDB file format. Existing 2.6 MDB databases, including an enabled accesslog database, must be exported with the 2.6 slapcat and imported with the 2.7 slapadd; never point a 2.7 container directly at a 2.6 data volume.
  • OpenLDAP 2.7 removed 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.
  • With LDAP_CONFIG_BACKEND=slapd.d, a populated LDAP_CONFIG_DIR becomes authoritative. Environment-based config changes are then seed-only and are n

Tag summary

Content type

Image

Digest

sha256:8d9d4ac67…

Size

14 MB

Last updated

6 days ago

docker pull chrroessner/openldap