From b5165061104ac18bf9c8ca4a37ec0488c412f79d Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 21 Aug 2026 17:40:41 +0300 Subject: [PATCH] Report the OpenDJ container healthy only once its bootstrap has succeeded The health check searched the root DSE, which answers as soon as setup starts the server - in the middle of bootstrap/setup.sh, before the backend of BASE_DN is created and its entries imported. A container therefore reported healthy while a search of BASE_DN still failed with "No Such Entry": that is what the CI "Docker test custom password" step hit when its poll landed in that window (run 32392485054), and what depends_on: service_healthy promises users it will not. run.sh now writes /opt/opendj/.bootstrap-complete, which the health check tests before it probes at all, so a container still bootstrapping costs no more than a test -f. It is written only where the step it stands for succeeded - the bootstrap, the replication it asked for, or the upgrade taken when starting over an instance that is already there - and cleared first, so a restarted container cannot inherit the marker of the previous run over the writable layer. What an instance is made of is now left to fail setup.sh, which had reported the status of its last command regardless of a backend it could not create; the optional schema and data LDIFs keep the tolerance they were written with. A bootstrap that fails leaves the container running to be looked at, but never healthy. The start period covers a bootstrap importing SAMPLE_DATA into a small container, which is minutes rather than the second it allowed for. --- opendj-packages/opendj-docker/Dockerfile | 11 +++++- .../opendj-docker/Dockerfile-alpine | 11 +++++- opendj-packages/opendj-docker/README.md | 25 +++++++++++++ .../opendj-docker/bootstrap/setup.sh | 29 ++++++++++++--- opendj-packages/opendj-docker/run.sh | 36 +++++++++++++++++-- 5 files changed, 102 insertions(+), 10 deletions(-) diff --git a/opendj-packages/opendj-docker/Dockerfile b/opendj-packages/opendj-docker/Dockerfile index ab4a45b279..9181389317 100644 --- a/opendj-packages/opendj-docker/Dockerfile +++ b/opendj-packages/opendj-docker/Dockerfile @@ -33,6 +33,8 @@ ENV OPENDJ_USER="opendj" ENV BACKEND_TYPE="je" ENV BACKEND_DB_DIRECTORY="db" #ENV SETUP_ARGS +# written by run.sh once the bootstrap has succeeded, read by the health check below +ENV BOOTSTRAP_COMPLETE="/opt/opendj/.bootstrap-complete" ARG OPENDJ_DIST_FILENAME=opendj.zip @@ -66,6 +68,13 @@ EXPOSE $PORT/tcp $LDAPS_PORT/tcp $ADMIN_PORT/tcp USER $OPENDJ_USER -HEALTHCHECK --interval=30s --timeout=30s --start-period=1s --retries=3 CMD opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 +# "healthy" has to mean the instance is ready to serve, not just that it answers: setup +# starts the server in the middle of the bootstrap, before the backend of BASE_DN is +# created and its entries imported, so probing the root DSE alone reports ready while a +# search of BASE_DN still fails with "No Such Entry". Testing the marker first also keeps +# the probe from launching a JVM every interval until the bootstrap is through. The start +# period is what a bootstrap importing SAMPLE_DATA into a small container can take; a +# probe that succeeds ends it early, and a bootstrap that failed never writes the marker. +HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD test -f "$BOOTSTRAP_COMPLETE" && opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 ENTRYPOINT ["/opt/opendj/run.sh"] diff --git a/opendj-packages/opendj-docker/Dockerfile-alpine b/opendj-packages/opendj-docker/Dockerfile-alpine index 65f1f0bb34..5f2635faae 100644 --- a/opendj-packages/opendj-docker/Dockerfile-alpine +++ b/opendj-packages/opendj-docker/Dockerfile-alpine @@ -33,6 +33,8 @@ ENV OPENDJ_USER="opendj" ENV BACKEND_TYPE="je" ENV BACKEND_DB_DIRECTORY="db" #ENV SETUP_ARGS +# written by run.sh once the bootstrap has succeeded, read by the health check below +ENV BOOTSTRAP_COMPLETE="/opt/opendj/.bootstrap-complete" ARG OPENDJ_DIST_FILENAME=opendj.zip @@ -70,6 +72,13 @@ EXPOSE $PORT/tcp $LDAPS_PORT/tcp $ADMIN_PORT/tcp USER $OPENDJ_USER -HEALTHCHECK --interval=30s --timeout=30s --start-period=1s --retries=3 CMD opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 +# "healthy" has to mean the instance is ready to serve, not just that it answers: setup +# starts the server in the middle of the bootstrap, before the backend of BASE_DN is +# created and its entries imported, so probing the root DSE alone reports ready while a +# search of BASE_DN still fails with "No Such Entry". Testing the marker first also keeps +# the probe from launching a JVM every interval until the bootstrap is through. The start +# period is what a bootstrap importing SAMPLE_DATA into a small container can take; a +# probe that succeeds ends it early, and a bootstrap that failed never writes the marker. +HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD test -f "$BOOTSTRAP_COMPLETE" && opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1 ENTRYPOINT ["/opt/opendj/run.sh"] diff --git a/opendj-packages/opendj-docker/README.md b/opendj-packages/opendj-docker/README.md index 9c40fc3459..0b85aa0471 100644 --- a/opendj-packages/opendj-docker/README.md +++ b/opendj-packages/opendj-docker/README.md @@ -12,11 +12,36 @@ Run image docker run -d -p 1389:1389 -p 1636:1636 -p 4444:4444 --name opendj openidentityplatform/opendj ``` +## Health check + +The image reports itself `healthy` once the server answers on `LDAPS_PORT` *and* the whole +bootstrap has succeeded - the instance, the `userRoot` backend over `BASE_DN`, whatever +`ADD_BASE_ENTRY` and `SAMPLE_DATA` asked to be imported into it, and the replication asked +for by `MASTER_SERVER`. Waiting for that status is therefore enough before the first search +of what the bootstrap was told to create: + +```bash +docker run -d --name opendj -e ADD_BASE_ENTRY=--addBaseEntry openidentityplatform/opendj +timeout 5m bash -c 'until [ "$(docker inspect -f "{{.State.Health.Status}}" opendj)" = healthy ]; do sleep 5; done' +``` + +In Compose the same is `depends_on: { opendj: { condition: service_healthy } }`. Note that +without `ADD_BASE_ENTRY` nothing creates the base entry, so `BASE_DN` is an empty suffix on +a healthy container - the health check itself searches the root DSE, which every instance +serves whatever it was set up to hold. + +A bootstrap that imports `SAMPLE_DATA` can take minutes on a small container, which is what +the start period allows for. A bootstrap that fails - or an upgrade that fails when starting +over an instance that is already there - never reports healthy: what failed is in `docker +logs`, and where the server is up at all the container is left running to be looked at, +turning `unhealthy` once the start period is over. + ## Environment Variables | Variable | Default Value | Description | |-------------------------|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ADD_BASE_ENTRY | | if set --addBaseEntry , creates base DN entry | +| SAMPLE_DATA | - | with ADD_BASE_ENTRY set, imports that many generated users under BASE_DN instead of the base entry alone | | PORT | 1389 | LDAP Listener Port | | LDAPS_PORT | 1636 | LDAPS Listener Port | | BASE_DN | dc=example,dc=com | OpenDJ Base DN | diff --git a/opendj-packages/opendj-docker/bootstrap/setup.sh b/opendj-packages/opendj-docker/bootstrap/setup.sh index a71782cad2..c571af0af4 100755 --- a/opendj-packages/opendj-docker/bootstrap/setup.sh +++ b/opendj-packages/opendj-docker/bootstrap/setup.sh @@ -1,5 +1,24 @@ #!/usr/bin/env bash +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Portions copyright 2026 3A Systems, LLC. + # Default setup script +# +# What the instance is made of - the server itself, the backend, the entries the backend was +# asked to hold - is left to fail this script, which is what run.sh reads to decide whether +# the container may report itself healthy. The optional schema and data LDIFs below keep the +# tolerance they were written with. echo "Setting up default OpenDJ instance" @@ -30,7 +49,7 @@ fi --acceptLicense \ --no-prompt \ --noPropertiesFile \ - $SETUP_ARGS + $SETUP_ARGS || exit 1 BACKEND_TYPE=${BACKEND_TYPE:-je} BACKEND_DB_DIRECTORY=${BACKEND_DB_DIRECTORY:-db} @@ -38,21 +57,21 @@ echo "creating backend: $BACKEND_TYPE db-directory: ${BACKEND_DB_DIRECTORY}" /opt/opendj/bin/dsconfig create-backend -h localhost -p $ADMIN_PORT --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" \ --backend-name=userRoot --type $BACKEND_TYPE --set base-dn:$BASE_DN --set "db-directory:$BACKEND_DB_DIRECTORY" \ - --set enabled:true --no-prompt --trustAll + --set enabled:true --no-prompt --trustAll || exit 1 if [ "$ADD_BASE_ENTRY" = "--addBaseEntry" ]; then BASE_TEMPLATE=$(mktemp) if [ ! -z ${SAMPLE_DATA} ]; then echo "generating sample data..." - /opt/opendj/bin/makeldif -o $BASE_TEMPLATE -c suffix="$BASE_DN" -c numusers=$SAMPLE_DATA /opt/opendj/template/config/MakeLDIF/example.template + /opt/opendj/bin/makeldif -o $BASE_TEMPLATE -c suffix="$BASE_DN" -c numusers=$SAMPLE_DATA /opt/opendj/template/config/MakeLDIF/example.template || exit 1 /opt/opendj/bin/import-ldif --ldifFile $BASE_TEMPLATE \ - --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" + --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" || exit 1 else echo "creating base entry..." BASE_TEMPLATE=$(mktemp) echo "branch: $BASE_DN" > $BASE_TEMPLATE /opt/opendj/bin/import-ldif --templateFile $BASE_TEMPLATE \ - --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" + --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" || exit 1 fi rm $BASE_TEMPLATE fi diff --git a/opendj-packages/opendj-docker/run.sh b/opendj-packages/opendj-docker/run.sh index b79e4e6dd9..9e028b2758 100755 --- a/opendj-packages/opendj-docker/run.sh +++ b/opendj-packages/opendj-docker/run.sh @@ -23,6 +23,17 @@ cd /opt/opendj +# The health check probes the server only once this marker is there, so that "healthy" +# means the instance is bootstrapped rather than merely listening: setup starts the +# server in the middle of the bootstrap, before the backend holding BASE_DN has been +# created. Nothing below writes it unless the step it stands for reported success, so a +# bootstrap that failed leaves the container running to be looked at, but never healthy. +# It is kept outside ./data because it records what this container has done, not what +# the volume holds - and a restart of a container replays this script over the writable +# layer the previous run left behind, so it is cleared before anything else. +BOOTSTRAP_COMPLETE=${BOOTSTRAP_COMPLETE:-/opt/opendj/.bootstrap-complete} +rm -f "$BOOTSTRAP_COMPLETE" + #if default data folder exists do not change it if [ ! -d ./db ]; then echo "/opt/opendj/data" >/opt/opendj/instance.loc && \ @@ -31,7 +42,13 @@ fi # Instance dir does exist? We start opendj without detach if [ -d ./data/config ]; then - sh ./upgrade -n + # nothing is bootstrapped here, the instance is already there - but a half-migrated one + # is not ready to serve either, so the marker follows the upgrade + if sh ./upgrade -n; then + touch "$BOOTSTRAP_COMPLETE" + else + echo "Upgrade failed, this container will not report itself healthy" + fi exec ./bin/start-ds --nodetach exit fi @@ -46,11 +63,18 @@ export ROOT_PASSWORD=${ROOT_PASSWORD:-password} BOOTSTRAP=${BOOTSTRAP:-/opt/opendj/bootstrap/setup.sh} echo "Running $BOOTSTRAP" -sh "${BOOTSTRAP}" +BOOTSTRAPPED=true +if ! sh "${BOOTSTRAP}"; then + BOOTSTRAPPED=false + echo "$BOOTSTRAP failed, this container will not report itself healthy" +fi # Check if OPENDJ_REPLICATION_TYPE var is set. If it is - replicate to that server if [ -n "${MASTER_SERVER}" ] && [ -n "${OPENDJ_REPLICATION_TYPE}" ]; then - /opt/opendj/bootstrap/replicate.sh + if ! /opt/opendj/bootstrap/replicate.sh; then + BOOTSTRAPPED=false + echo "Replication setup failed, this container will not report itself healthy" + fi fi # Check if keystores are mounted as a volume, and if so @@ -63,6 +87,12 @@ if [ -d "${SECRET_VOLUME}" ]; then cp -f ${SECRET_VOLUME}/key* ${SECRET_VOLUME}/trust* ./data/config 2>/dev/null fi +# Everything the instance was asked to be set up with - its backend, its base entry, its +# replication - is in place from here on, so the health check may start probing the server +if [ "$BOOTSTRAPPED" = true ]; then + touch "$BOOTSTRAP_COMPLETE" +fi + # Opendj is probably already started in detach mode at the install if (bin/status -n | grep Started); then echo "OpenDJ is started"