From 92cfe956e41a0beee435305c090ffc9daad72445 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 25 Aug 2026 08:00:34 +0000
Subject: [PATCH] Report the OpenDJ container healthy only once its bootstrap has succeeded (#898)

---
 opendj-packages/opendj-docker/run.sh             |   36 ++++++++++++++++-
 opendj-packages/opendj-docker/Dockerfile-alpine  |   11 +++++
 opendj-packages/opendj-docker/README.md          |   25 ++++++++++++
 opendj-packages/opendj-docker/bootstrap/setup.sh |   29 ++++++++++++--
 opendj-packages/opendj-docker/Dockerfile         |   11 +++++
 5 files changed, 102 insertions(+), 10 deletions(-)

diff --git a/opendj-packages/opendj-docker/Dockerfile b/opendj-packages/opendj-docker/Dockerfile
index ab4a45b..9181389 100644
--- a/opendj-packages/opendj-docker/Dockerfile
+++ b/opendj-packages/opendj-docker/Dockerfile
@@ -33,6 +33,8 @@
 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 @@
 
 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 65f1f0b..5f2635f 100644
--- a/opendj-packages/opendj-docker/Dockerfile-alpine
+++ b/opendj-packages/opendj-docker/Dockerfile-alpine
@@ -33,6 +33,8 @@
 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 @@
 
 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 9c40fc3..0b85aa0 100644
--- a/opendj-packages/opendj-docker/README.md
+++ b/opendj-packages/opendj-docker/README.md
@@ -12,11 +12,36 @@
 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 a71782c..c571af0 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 @@
   --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 @@
 
 /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 b79e4e6..9e028b2 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 @@
 
 # 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 @@
 
 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 @@
   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"

--
Gitblit v1.10.0