Update and Upgrade 389 Directory Server Safely

Update 389 Directory Server safely with dnf, pre-upgrade health checks, verified backups, standalone and rolling replica updates, post-upgrade validation, and recovery when a package update fails.

Published

Updated

Read time 25 min read

Reviewed byDeepak Prasad

389 Directory Server package update workflow with health check, backup, rolling host update, and post-upgrade validation

A routine 389 Directory Server update is normally performed through the operating-system package manager. Supported updates install newer 389-ds-base RPMs and related dependencies; there is no separate manual upgrade utility for a standard minor package refresh.

Administrators typically need answers to these questions before maintenance:

  • Is this a package update or a migration?
  • Does dnf update 389-ds-base restart Directory Server?
  • Can every replica be updated at once?
  • What should I back up first?
  • How do I verify LDAP, TLS, and replication after the RPM transaction?
  • What is a safe rollback path when startup fails?

This guide answers those with a step-by-step workflow for standalone and replicated deployments.

On Red Hat Directory Server and equivalent RHEL-family RPM deployments, the package transaction restarts the dirsrv services for every instance on the host. On Debian, SUSE, or another packaging system, confirm the package's restart behavior and explicitly verify every instance after the update.

You do not need two physical servers for the standalone update path. You do need a backup, baseline, and validation for every Directory Server instance on the host being updated.

Lab layout

Host What this guide uses it for
ldap1.example.com Main walkthrough in Steps 2–5 and 7. The RPM transaction restarts every instance dsctl -l reports on this host.
ldap2.example.com Rolling update example in Step 6 only

Steps 2–5 and 7 use ldap1 as the primary instance name. When this host runs more than one instance, the examples use a loop over dsctl -l instead of naming each instance in prose.

In production, give each instance a unique short name on every host. Reusing the same name on two hosts, as this lab does with ldap2, makes tutorials harder to follow than the software requires.

The safe workflow is: classify the change, check health, create a verified backup, update one host at a time in replicated topologies, and validate before returning clients.

Before you start:

Tested on: Rocky Linux 10.2; 389 Directory Server 3.2.0.


Step 1: Decide whether you need an update or migration

Administrators search for both update and upgrade. In this article, update means a supported package refresh on the same host and instance layout. Upgrade also refers to that routine package path unless the table below points to migration.

Current situation Required approach
New package from the existing supported repository In-place package update
Minor release update on the same supported OS In-place package update
Multiple replicated servers on separate hosts Step 6 rolling host-by-host update
Major Red Hat Directory Server generation change Migrate to a new server or export and import LDIF
Moving to a new operating-system major version Migrate to a new server
Moving the instance to another server Migrate to a new server

If your situation matches a migration row, use that guide instead of continuing with the package-update steps below.

Use in-place package update when the target 389-ds-base build is supported on your current OS and you are not changing database backend or product generation.


Step 2: Check the installed and available versions

Record the installed build before any maintenance. Directory Server does not provide a separate dsctl --version subcommand; read the package NEVRA instead.

Check the installed 389-ds-base build:

bash
rpm -q 389-ds-base
output
389-ds-base-3.2.0-8.el10_2.x86_64

The NEVRA confirms the installed minor release on this host.

List the Directory Server packages included in the update transaction:

bash
rpm -q 389-ds-base 389-ds-base-libs python3-lib389
output
389-ds-base-3.2.0-8.el10_2.x86_64
389-ds-base-libs-3.2.0-8.el10_2.x86_64
python3-lib389-3.2.0-8.el10_2.noarch

All three packages move together during a routine dnf update 389-ds-base transaction.

Check Cockpit integration separately. It is optional but updated together with Directory Server on Red Hat deployments when installed:

bash
rpm -q cockpit-389-ds
output
package cockpit-389-ds is not installed

The package cockpit-389-ds is not installed message means the optional Cockpit integration is absent. Update only 389-ds-base in that case. When Cockpit is installed, include cockpit-389-ds in the same dnf update transaction.

Review repository metadata for the installed build:

bash
dnf info 389-ds-base
output
Name         : 389-ds-base
Version      : 3.2.0
Release      : 8.el10_2
Repository   : @System
Summary      : 389 Directory Server (base)

Repository: @System identifies the package currently installed on the host. It does not show whether an update is available; use dnf check-update or the corresponding DNF5 check-upgrade command for that decision.

Ask the package manager whether a newer build is available:

bash
dnf --refresh check-update 389-ds-base

DNF documents these exit codes for update checks:

  • 0 — no updates are available
  • 100 — updates are available
  • any other value — a command or repository error

A robust interactive example:

bash
dnf --refresh check-update 389-ds-base
rc=$?

case "$rc" in
    0)
        echo "No 389-ds-base update is available"
        ;;
    100)
        echo "A 389-ds-base update is available"
        ;;
    *)
        echo "DNF update check failed with exit code $rc" >&2
        exit "$rc"
        ;;
esac

When the check returns exit code 0 with no output, the installed build already matches the newest package in the enabled repositories.

Confirm the host operating-system release because package compatibility is tied to the platform:

bash
grep -E '^(NAME|VERSION)=' /etc/os-release
output
NAME="Rocky Linux"
VERSION="10.2 (Red Quartz)"

Rocky Linux 10.2 is the platform this lab used for the tested upgrade path.

Use dnf command to compare installed and available versions before scheduling the maintenance window. If the update check reports exit code 100, read the release notes for the newer 389-ds-base NEVRA before proceeding.

List every 389-ds-base build published in the enabled repositories:

bash
dnf repoquery 389-ds-base --qf '%{version}-%{release}'
output
3.2.0-6.el10
3.2.0-7.el10_2
3.2.0-8.el10_2

Install the newest supported NEVRA for your platform. Older builds remain in the repository history, but production hosts should move forward to the current supported release.

The newest Rocky Linux 10.2 AppStream build in this lab is 3.2.0-8.el10_2. Upstream may publish newer releases such as 3.2.1 for Fedora before your distribution repository carries them. Routine updates come from the distribution repository you have enabled, not directly from the Port389 download page.


Step 3: Prepare a pre-upgrade checklist

Complete these checks on every host you plan to update.

Review release notes and known issues for the target 389-ds-base build. Confirm the build is supported on your OS subscription and repository set.

Check free disk space under database, backup, and log paths:

bash
df -h /var/lib/dirsrv/slapd-ldap1/db /var/lib/dirsrv/slapd-ldap1/bak /var/log/dirsrv/slapd-ldap1
output
Filesystem            Size  Used Avail Use% Mounted on
/dev/mapper/rlm-root   14G   11G  2.4G  83% /
/dev/mapper/rlm-root   14G   11G  2.4G  83% /
/dev/mapper/rlm-root   14G   11G  2.4G  83% /
bash
du -sh /var/lib/dirsrv/slapd-*/db
output
9.0M	/var/lib/dirsrv/slapd-ldap1/db
4.9M	/var/lib/dirsrv/slapd-ldap2/db

This lab had approximately 2.4 GiB free on /. That was sufficient only because the measured backup and package transaction were small. Do not use 2.4 GiB as a general threshold. Confirm space for a complete backup of every affected instance, the DNF download and cache, temporary database activity, and expected log growth.

After the DNF preview in Step 5, use its reported download size together with the measured backup sizes and a reasonable safety margin. Native backup operates independently for each instance, so a multi-instance host needs capacity for each required recovery point.

Record custom schema files under /etc/dirsrv/slapd-INSTANCE/schema/ and any third-party plug-ins you enabled.

List every Directory Server instance on the host:

Offline instance work in this section uses dsctl commands.

bash
dsctl -l
output
slapd-ldap1
slapd-ldap2

This host runs more than one instance in the lab. The dsctl -l output lists every short name the package transaction will restart. Use that list rather than assuming a single instance. Retired instance directories can remain in the list even when their dirsrv@ unit is inactive.

Record every production instance on the host before you change packages:

Host Instance from dsctl -l Backup required Baseline required
ldap1.example.com Every name returned by dsctl -l on that host Yes Yes

Save pre-upgrade baselines

Save timestamped evidence before changing packages. Dynamic counters and timestamps can differ after restart; the comparison is intended to surface new warnings, missing entries, disabled agreements, or changed package versions.

Create a baseline directory. Mode 700 limits read access to root:

bash
baseline="/root/389ds-upgrade-baseline-$(date +%Y%m%d-%H%M%S)"
mkdir -m 700 "$baseline"
bash
printf '%s\n' "$baseline" > /root/389ds-upgrade-baseline.latest

The command exits silently when the directory is created. The .latest file lets you reload the baseline path after the maintenance window.

Record every instance that is active before the update. RHDS package updates restart the Directory Server services for all instances on the host, so every instance that was active beforehand must be accounted for afterward:

bash
active_instances="$baseline/active-instances.txt"
: > "$active_instances"

while IFS= read -r listed_instance; do
    instance="${listed_instance#slapd-}"

    if systemctl is-active --quiet "dirsrv@${instance}.service"; then
        printf '%s\n' "$instance" >> "$active_instances"
        dsctl "$instance" status
    fi
done < <(dsctl -l)

test -s "$active_instances" || {
    echo "No active Directory Server instances found" >&2
    exit 1
}
output
Instance "ldap1" is running
Instance "ldap2" is running

The sample output shows two local instances. After the package transaction, verify every name saved in $active_instances restarted successfully.

Save host-level package state once per host:

bash
rpm -q 389-ds-base 389-ds-base-libs python3-lib389 | tee "$baseline/packages.txt"
output
389-ds-base-3.2.0-8.el10_2.x86_64
389-ds-base-libs-3.2.0-8.el10_2.x86_64
python3-lib389-3.2.0-8.el10_2.noarch

The saved NEVRAs record the build installed before the update transaction.

Save health-check output for the primary supplier instance:

bash
dsctl ldap1 healthcheck | tee "$baseline/ldap1-healthcheck.txt"
output
Healthcheck complete.
No issues found.

Save the entry count for the suffix your applications use:

bash
ldapsearch -LLL -o ldif-wrap=no -Y EXTERNAL -H ldapi://%2Frun%2Fslapd-ldap1.socket -b "dc=example,dc=com" "(objectClass=*)" dn | grep -Ec '^dn::? ' | tee "$baseline/ldap1-entry-count.txt"
output
769

Repeat status, health-check, and entry-count checks for every additional instance dsctl -l reports on the same host. Store each result in a separate file under $baseline/.

Save replication state for the supplier instance:

Online configuration in this section uses dsconf commands.

bash
dsconf -D "cn=Directory Manager" -y /root/dm.pw ldap1 replication status --suffix "dc=example,dc=com" | tee "$baseline/ldap1-replication.txt"
output
{'agmt-name': ['ldap1-to-consumer1'], 'replica': ['ldap1.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}
{'agmt-name': ['ldap1-to-supplier2'], 'replica': ['ldap2.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}

The ldap1-to-supplier2 agreement targets the remote host ldap2.example.com:1389. Step 6 covers that host separately.

After updating, capture equivalent post-upgrade evidence and compare it against the baseline in Step 7.

The health check detects selected known potential problems across configuration, backends, plug-ins, TLS, replication, and related subsystems. It does not replace application-specific validation.

The health-check tool analyzes the instance for potential known issues; it is not an exhaustive proof of every feature or application workflow. Treat a new warning after update as a regression until you confirm otherwise.

For replicated deployments, also confirm:

  • Every replication agreement reports a healthy state.
  • No replica shows unexplained lag.
  • At least one other supplier can accept writes while one host is updated.
  • The host being updated can be removed from load balancers or application connection lists.

List agreements and read synchronization state on ldap1 in this lab:

bash
dsconf -D "cn=Directory Manager" -y /root/dm.pw ldap1 replication status --suffix dc=example,dc=com
output
{'agmt-name': ['ldap1-to-consumer1'], 'replica': ['ldap1.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}
{'agmt-name': ['ldap1-to-supplier2'], 'replica': ['ldap2.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}

Both agreements show In Synchronization with zero lag in this lab snapshot.

Schedule a maintenance window and confirm applications can tolerate a service restart on standalone hosts or a brief host removal in replicated topologies.


Step 4: Create and verify a pre-upgrade backup

Create a native backup before updating. The complete online, offline, checksum, and recovery workflow lives in Back Up and Restore 389 Directory Server.

Native backup is instance-specific, while the RPM transaction restarts every Directory Server instance on the host. Create and verify a backup for each instance listed in $baseline/active-instances.txt:

bash
baseline="$(cat /root/389ds-upgrade-baseline.latest)"
active_instances="$baseline/active-instances.txt"

while IFS= read -r instance; do
    stamp="$(date +%Y%m%d-%H%M%S)"
    backup="/var/lib/dirsrv/slapd-${instance}/bak/${instance}-${stamp}-pre-upgrade"

    dsconf "$instance" backup create "$backup"

    test -s "$backup/data.mdb"
    test -s "$backup/INFO.mdb"
    test -s "$backup/config_files/dse.ldif"

    (
        cd "$backup"
        find . -type f ! -name SHA256SUMS -print0 |
            sort -z |
            xargs -0 sha256sum > SHA256SUMS

        sha256sum -c SHA256SUMS
    )
done < "$active_instances"

The official RHDS procedure uses dsconf INSTANCE backup create for a running local instance. Run the backup as root on the host that owns the instance. If your environment requires explicit Directory Manager authentication, maintain a separate password file or .dsrc configuration for each instance rather than reusing one shared file such as /root/dm.pw.

output
The backup create task has finished successfully
./INFO.mdb: OK
./data.mdb: OK
./config_files/dse.ldif: OK
...
The backup create task has finished successfully
./INFO.mdb: OK
./data.mdb: OK
./config_files/dse.ldif: OK
...

Excerpt: each instance prints its own backup task result and checksum verification.

List backups to confirm the archives exist on the primary instance:

bash
dsctl ldap1 backups
output
Backup: /var/lib/dirsrv/slapd-ldap1/bak/ldap1-20260718-pre-upgrade - 2026-07-18 11:41:21 (9.3M)

Run dsctl INSTANCE backups for every other instance name dsctl -l returned on the host.

Checking expected files and validating the checksum manifest confirms that the local archive is readable. A periodic isolated restore remains the strongest proof of recoverability.


Step 5: Update a standalone 389 Directory Server

This lab uses instance ldap1 on Rocky Linux 10.2 with dnf. Adapt the package names on Debian (apt), SUSE (zypper), or other distributions while keeping the same preparation and verification sequence.

Red Hat documents package installation as the supported minor-update procedure. Installing newer 389-ds-base RPMs is the upgrade step. There is no separate Directory Server upgrade command after a routine package refresh.

Preview the update

Preview the transaction without installing packages.

When your repository offers a newer build, dnf lists the target NEVRA:

bash
dnf update 389-ds-base --assumeno

Sample output when updating from 3.2.0-7.el10_2 to 3.2.0-8.el10_2:

output
Upgrading:
 389-ds-base            x86_64       3.2.0-8.el10_2       appstream       2.9 M
 389-ds-base-libs       x86_64       3.2.0-8.el10_2       appstream       1.5 M
 python3-lib389         noarch       3.2.0-8.el10_2       appstream       1.3 M

Transaction Summary
================================================================================
Upgrade  3 Packages

Total download size: 5.7 M
Operation aborted.

When the host already matches the newest repository build, preview instead shows:

output
Dependencies resolved.
Nothing to do.
Complete!

Read the transaction summary for the target 389-ds-base NEVRA, dependency changes, and repository source. If Cockpit is installed, include it in the same preview:

bash
dnf update 389-ds-base cockpit-389-ds --assumeno

When Cockpit is installed, the preview lists the same Directory Server packages plus cockpit-389-ds.

Install the update

Install the update during the maintenance window.

Without Cockpit:

bash
dnf update 389-ds-base -y

With Cockpit installed:

bash
dnf update 389-ds-base cockpit-389-ds -y

This lab host used the path without Cockpit. When a newer build is pending, the transaction upgrades 389-ds-base, 389-ds-base-libs, and python3-lib389.

Sample output when updating from 3.2.0-7.el10_2 to 3.2.0-8.el10_2:

output
Upgrading:
 389-ds-base            x86_64       3.2.0-8.el10_2       appstream       2.9 M
 389-ds-base-libs       x86_64       3.2.0-8.el10_2       appstream       1.5 M
 python3-lib389         noarch       3.2.0-8.el10_2       appstream       1.3 M

Upgraded:
  389-ds-base-3.2.0-8.el10_2.x86_64     389-ds-base-libs-3.2.0-8.el10_2.x86_64
  python3-lib389-3.2.0-8.el10_2.noarch

Complete!

On RHEL-family RPM deployments, the transaction automatically restarts the dirsrv services for every instance on the host. Do not run dscreate or recreate existing instances after a routine package update.

Confirm that every instance restarted

Check that systemd restarted each instance during the package transaction:

bash
journalctl -u [email protected] --since "5 minutes ago" --no-pager
output
Jul 18 11:49:25 ldap1.example.com systemd[1]: Stopping [email protected] - 389 Directory Server ldap1....
Jul 18 11:49:29 ldap1.example.com systemd[1]: Stopped [email protected] - 389 Directory Server ldap1..
Jul 18 11:49:32 ldap1.example.com systemd[1]: Starting [email protected] - 389 Directory Server ldap1....
Jul 18 11:49:34 ldap1.example.com systemd[1]: Started [email protected] - 389 Directory Server ldap1..
Jul 18 11:49:34 ldap1.example.com ns-slapd[107370]: [18/Jul/2026:11:49:34.012451217 +0530] - INFO - slapd_daemon - slapd started.  Listening on All Interfaces port 389 for LDAP requests

The stop and start lines confirm systemd restarted ldap1 during the RPM transaction.

Confirm every instance that was active before the update restarted successfully:

bash
baseline="$(cat /root/389ds-upgrade-baseline.latest)"
active_instances="$baseline/active-instances.txt"
failed=0

while IFS= read -r instance; do
    if ! systemctl is-active --quiet "dirsrv@${instance}.service"; then
        echo "ERROR: dirsrv@${instance}.service did not restart" >&2
        failed=1
        continue
    fi

    dsctl "$instance" status || failed=1
done < "$active_instances"

if [ "$failed" -ne 0 ]; then
    exit 1
fi
output
Instance "ldap1" is running
Instance "ldap2" is running

Every instance saved in $active_instances is active and running after the RPM transaction.

Confirm the transaction installed the target NEVRAs:

bash
rpm -q 389-ds-base 389-ds-base-libs python3-lib389
output
389-ds-base-3.2.0-8.el10_2.x86_64
389-ds-base-libs-3.2.0-8.el10_2.x86_64
python3-lib389-3.2.0-8.el10_2.noarch

All three packages now show release 8.el10_2.


Step 6: Perform a rolling update in a replicated topology

A 389 Directory Server rolling upgrade works at the host level. On RHEL-family RPM deployments, updating 389-ds-base on one server restarts every Directory Server instance on that host, not a single suffix in isolation.

This lab uses ldap1.example.com as the primary supplier and ldap2.example.com as a remote supplier. Update one host at a time: prepare and verify the remote host first, confirm replication, then repeat on ldap1.example.com. Each host reuses the backup, baseline, package-update, and validation commands from Step 3 through Step 5 and Step 7.

Confirm replication health before the first host

From a supplier that will stay online while you update the remote host, list agreements and read synchronization state:

bash
dsconf -D "cn=Directory Manager" -y /root/dm.pw ldap1 replication status --suffix dc=example,dc=com
output
{'agmt-name': ['ldap1-to-supplier2'], 'replica': ['ldap2.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}

Confirm the supplier and the host you plan to update first are synchronized. Use the hostname and LDAP port from your agreement output:

bash
ds-replcheck state -D "cn=Directory Manager" -y /root/dm.pw -m ldap://ldap1.example.com:389 -r ldap://ldap2.example.com:1389 -b "dc=example,dc=com"
output
Replication State: Supplier and Replica are in perfect synchronization

Do not start the package transaction while agreements report unexplained lag or a disabled replica.

Update the remote host first

Remove ldap2.example.com from the load balancer or application connection list. On the remote host, list every instance the RPM transaction will restart:

bash
# Run on ldap2.example.com:
dsctl -l
output
slapd-ldap2

Create and verify a pre-upgrade backup for each production instance on that host, using the loop from Step 4. Save a baseline directory on the remote host the same way you did in Step 3.

Preview and install the package update during the maintenance window:

bash
# Run on ldap2.example.com:
dnf update 389-ds-base --assumeno

When the host already matches the newest repository build, preview shows:

output
Dependencies resolved.
Nothing to do.
Complete!

When a newer build is pending, install it with dnf update 389-ds-base -y, including cockpit-389-ds when Cockpit is installed.

Confirm systemd restarted the instance during the transaction:

bash
# Run on ldap2.example.com:
systemctl is-active [email protected]
output
active
bash
dsctl ldap2 status
output
Instance "ldap2" is running

Review the recent service journal for a stop and start pair:

bash
journalctl -u [email protected] --since "1 hour ago" --no-pager
output
Jul 18 22:16:28 ldap2.example.com systemd[1]: Stopped [email protected] - 389 Directory Server ldap2..
Jul 18 22:16:29 ldap2.example.com systemd[1]: Starting [email protected] - 389 Directory Server ldap2....
Jul 18 22:16:30 ldap2.example.com systemd[1]: Started [email protected] - 389 Directory Server ldap2..

Verify replication before you update the next host

From ldap1.example.com, confirm the updated supplier caught up before you return it to client traffic:

bash
dsconf -D "cn=Directory Manager" -y /root/dm.pw ldap1 replication status --suffix dc=example,dc=com
output
{'agmt-name': ['ldap1-to-supplier2'], 'replica': ['ldap2.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}
bash
ds-replcheck state -D "cn=Directory Manager" -y /root/dm.pw -m ldap://ldap1.example.com:389 -r ldap://ldap2.example.com:1389 -b "dc=example,dc=com"
output
Replication State: Supplier and Replica are in perfect synchronization

In Synchronization with zero lag and a successful ds-replcheck result mean the updated host can rejoin client traffic. Complete Step 7 on that host before you move to the next one.

Update the remaining host

Repeat the same sequence on ldap1.example.com:

  • Confirm surviving suppliers are healthy
  • Drain the host from client traffic
  • Back up and baseline every instance saved in $baseline/active-instances.txt on that host
  • Run dnf update 389-ds-base
  • Confirm every dirsrv@ unit restarted
  • Verify replication and application connectivity before you update another host

Important points:

  • Update one host at a time
  • Do not update every supplier simultaneously
  • Mixed-version operation should be temporary
  • Do not enable version-specific features until required replicas run the new build
  • A rolling update minimizes interruption but does not guarantee zero downtime; availability depends on healthy replicas and client failover

Run restore and reinitialization workflows from Restore a replicated server only when an updated replica cannot be returned to service.


Step 7: Verify the upgrade

Compare before-and-after state even when the package NEVRA does not change. A failed restart or plug-in regression is easier to spot against a saved baseline.

Reload the baseline path you saved in Step 3:

bash
baseline="$(cat /root/389ds-upgrade-baseline.latest)"

test -d "$baseline" || {
    echo "Baseline directory not found: $baseline" >&2
    exit 1
}

Create a post-upgrade evidence directory using the same layout:

bash
post="/root/389ds-upgrade-post-$(date +%Y%m%d-%H%M%S)"
mkdir -m 700 "$post"

Save post-upgrade package state:

bash
rpm -q 389-ds-base 389-ds-base-libs python3-lib389 | tee "$post/packages.txt"

Compare package NEVRAs explicitly. They should change only when an update was available:

bash
echo "Before:"
cat "$baseline/packages.txt"

echo "After:"
cat "$post/packages.txt"

Save the post-upgrade health check for ldap1:

bash
dsctl ldap1 healthcheck | tee "$post/ldap1-healthcheck.txt"
output
Healthcheck complete.
No issues found.

Save the post-upgrade entry count for the primary instance:

bash
ldapsearch -LLL -o ldif-wrap=no -Y EXTERNAL -H ldapi://%2Frun%2Fslapd-ldap1.socket -b "dc=example,dc=com" "(objectClass=*)" dn | grep -Ec '^dn::? ' | tee "$post/ldap1-entry-count.txt"
output
769

Repeat health-check and entry-count comparisons for every additional instance dsctl -l reports. Store each result under $post/ with the instance name in the filename.

Save replication state:

bash
dsconf -D "cn=Directory Manager" -y /root/dm.pw ldap1 replication status --suffix "dc=example,dc=com" | tee "$post/ldap1-replication.txt"
output
{'agmt-name': ['ldap1-to-consumer1'], 'replica': ['ldap1.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}
{'agmt-name': ['ldap1-to-supplier2'], 'replica': ['ldap2.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}

Compare stable results with exact diff:

bash
diff -u "$baseline/ldap1-healthcheck.txt" "$post/ldap1-healthcheck.txt"
bash
diff -u "$baseline/ldap1-entry-count.txt" "$post/ldap1-entry-count.txt"

In this lab, the ldap1 entry-count comparison produced no output, which confirms the primary supplier kept the same directory data after the package transaction.

Replication output contains state that may change during restart and catch-up. Save both reports, but validate the meaningful fields rather than requiring byte-for-byte equality:

  • Every expected agreement still exists
  • replica-enabled is on
  • Status returns to In Synchronization
  • Lag returns to the accepted threshold
  • No new last-update error remains
Check What to confirm
Package version Installed NEVRAs match the target build from the transaction summary
Instance status Every local instance reports running
Health check No new material errors compared with the baseline for each instance you checked
LDAP search Entry counts match the saved baseline for each instance
LDAPS connection ldapwhoami succeeds with your client trust anchor
STARTTLS ldapwhoami -ZZ succeeds
Authentication Application bind DNs still work
Replication Agreements return to In Synchronization before the next host is updated
Error log No unexplained startup or plug-in errors after restart

Confirm the installed packages match the target build:

bash
rpm -q 389-ds-base 389-ds-base-libs python3-lib389
output
389-ds-base-3.2.0-8.el10_2.x86_64
389-ds-base-libs-3.2.0-8.el10_2.x86_64
python3-lib389-3.2.0-8.el10_2.noarch

The post-upgrade NEVRAs match the target build from the transaction summary.

Confirm every instance that was active before the update is still running:

bash
active_instances="$baseline/active-instances.txt"
failed=0

while IFS= read -r instance; do
    if ! systemctl is-active --quiet "dirsrv@${instance}.service"; then
        echo "ERROR: dirsrv@${instance}.service did not restart" >&2
        failed=1
        continue
    fi

    dsctl "$instance" status || failed=1
done < "$active_instances"

if [ "$failed" -ne 0 ]; then
    exit 1
fi
output
Instance "ldap1" is running
Instance "ldap2" is running

Run the health check again and confirm no new warnings appeared:

bash
dsctl ldap1 healthcheck
output
Healthcheck complete.
No issues found.

The result still matches the pre-upgrade baseline.

Test directory data with an authenticated search and compare against the saved baseline entry count for ldap1:

bash
ldapsearch -LLL -o ldif-wrap=no -x -H ldap://127.0.0.1:389 -D "cn=Directory Manager" -y /root/dm.pw -b "dc=example,dc=com" "(objectClass=*)" dn | grep -Ec '^dn::? '
output
769

Repeat the same search for every other instance on the host. Use each instance's LDAP port or its ldapi socket path from dsctl -l.

The count for ldap1 matches the value saved in the baseline.

Test LDAPS and STARTTLS

A successful anonymous TCP connection is not enough. Certificate validation and an authenticated LDAP operation should succeed with the same trust anchor your clients use.

For LDAPS:

Identity checks in this section use the ldapwhoami command.

bash
LDAPTLS_CACERT="/tmp/389ds-selfsigned-ca.pem" \
ldapwhoami -x -H ldaps://ldap1.example.com:636 -D "cn=Directory Manager" -y /root/dm.pw
output
dn: cn=directory manager

The server accepted the LDAPS connection and authenticated the bind.

For STARTTLS:

bash
LDAPTLS_CACERT="/tmp/389ds-selfsigned-ca.pem" \
ldapwhoami -x -ZZ -H ldap://ldap1.example.com:389 -D "cn=Directory Manager" -y /root/dm.pw
output
dn: cn=directory manager

STARTTLS upgraded the plain LDAP connection before authentication succeeded.

Point LDAPTLS_CACERT at the CA file your clients trust.

Re-check replication on suppliers:

bash
dsconf -D "cn=Directory Manager" -y /root/dm.pw ldap1 replication status --suffix dc=example,dc=com
output
{'agmt-name': ['ldap1-to-consumer1'], 'replica': ['ldap1.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}
{'agmt-name': ['ldap1-to-supplier2'], 'replica': ['ldap2.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}

Review recent service and error logs for configuration, schema, plug-in, database, TLS, permission, and replication failures:

bash
journalctl -u [email protected] --since "30 minutes ago" --no-pager
output
Jul 18 11:49:32 ldap1.example.com systemd[1]: Starting [email protected] - 389 Directory Server ldap1....
Jul 18 11:49:34 ldap1.example.com systemd[1]: Started [email protected] - 389 Directory Server ldap1..
Jul 18 11:49:34 ldap1.example.com ns-slapd[107370]: [18/Jul/2026:11:49:34.012451217 +0530] - INFO - slapd_daemon - slapd started.  Listening on All Interfaces port 389 for LDAP requests

Then read the Directory Server errors log:

bash
tail -n 5 /var/log/dirsrv/slapd-ldap1/errors
output
[18/Jul/2026:11:49:34.012451217 +0530] - INFO - slapd_daemon - slapd started.  Listening on All Interfaces port 389 for LDAP requests
[18/Jul/2026:11:49:34.020255238 +0530] - INFO - slapd_daemon - Listening on All Interfaces port 636 for LDAPS requests

After the tested upgrade, replication agreements returned to In Synchronization with zero lag.


Step 8: Recover from a failed update

Use this decision sequence when a package update does not return the directory to service:

  1. Keep the affected host out of client rotation.
  2. Save journalctl, Directory Server errors logs, and the DNF transaction details.
  3. Determine whether the failure is package installation, startup configuration, custom plug-in compatibility, TLS, replication, or database-related.
  4. Do not restore database files merely because the service failed to start; first determine whether the existing database remains intact.
  5. On a failed replica, prefer reinitialization from a healthy supplier when the topology remains current.
  6. On a standalone server, restore the verified backup only with a compatible Directory Server package and database implementation.
  7. Do not use dnf downgrade or dnf history undo as an automatic rollback. Obtain a distribution-supported rollback plan when the newer package may have changed configuration or database state.
Failure First response
Service fails to start Review journalctl -u dirsrv@INSTANCE and the Directory Server errors log
Configuration attribute rejected Compare dse.ldif and release notes for the target build
Custom plug-in fails Check plug-in compatibility with the new package
LDAPS fails Review certificates, trust stores, protocols, and ciphers
One replica fails Keep it out of service and use a healthy supplier
Data or database failure Restore from the verified pre-upgrade backup
Package regression Follow the distribution-supported rollback procedure

Capture the package transaction:

bash
dnf history info last
output
Command Line   : update -y 389-ds-base
Packages Altered:
    Upgrade  389-ds-base-3.2.0-8.el10_2.x86_64      @appstream
    Upgraded 389-ds-base-3.2.0-7.el10_2.x86_64      @@System

The Packages Altered section records the exact NEVRA change for support tickets.

Capture service logs to a file for later review:

bash
journalctl -u [email protected] --since "1 hour ago" --no-pager > /root/ldap1-failed-upgrade-journal.txt

The redirect exits silently when the journal file is written.

Preserve the Directory Server errors log:

bash
cp -a /var/log/dirsrv/slapd-ldap1/errors /root/ldap1-failed-upgrade-errors.log

cp -a also exits silently when the copy completes.

Red Hat documents package installation as the entire supported minor-update procedure. Major product-generation changes require migration to a separately created instance. Upstream 3.2.0 likewise states that routine RPM upgrades require no separate post-install upgrade step.

For a failed replica in a replicated topology, reinitializing from a healthy supplier is often safer than restoring an old replicated database. Use Restore a replicated server when in-place recovery is not appropriate.

Troubleshoot common upgrade problems

Symptom Likely cause Fix
dirsrv@INSTANCE inactive after update Configuration, schema, plug-in, port, permission, or SELinux error Read journalctl and /var/log/dirsrv/slapd-INSTANCE/errors
LDAP works but LDAPS fails Certificate expiry, hostname mismatch, or TLS policy change Review certificate management and TLS settings
Replication stops on one updated host Agreement disabled, network/TLS failure, clock skew, or version mismatch Check dsconf replication status, time sync, and connectivity before reinitializing
Cockpit shows stale data cockpit-389-ds not updated or browser cache Update the Cockpit package and verify with dsctl / dsconf
New health-check warning Regression or previously ignored condition Compare with the pre-upgrade health-check output

Directory Server does not start after the update

Check systemd first:

bash
systemctl status [email protected] --no-pager
output
[email protected] - 389 Directory Server ldap1.
     Loaded: loaded (/usr/lib/systemd/system/[email protected]; enabled; preset: disabled)
     Active: active (running) since Sat 2026-07-18 11:49:34 IST; 2h ago
   Main PID: 107370 (ns-slapd)

An active (running) state with a recent start time points to a successful restart; failed or inactive means you should read the errors log next.

Then read the errors log for schema, plug-in, database, and permission messages.

Replication stops after one server is updated

Inspect agreement state before reinitializing:

bash
dsconf -D "cn=Directory Manager" -y /root/dm.pw ldap1 replication status --suffix dc=example,dc=com
output
{'agmt-name': ['ldap1-to-supplier2'], 'replica': ['ldap2.example.com:1389'], 'replica-enabled': ['on'], 'replication-status': ['In Synchronization'], 'replication-lag-time': ['00:00:00'], 'last-update-status': ['Error (0) Replica acquired successfully: Incremental update succeeded']}

Look for replica-enabled: off, non-zero lag, or a last-update error before you reinitialize. Mixed-version operation is expected briefly during a rolling update, but agreements should return to In Synchronization before you update the next host.

Health check reports new warnings

Compare the post-update report with the baseline you saved before maintenance. Resolve only warnings that are new or that block directory service.


What's next

After you complete this guide, continue with:

Summary

  1. Decide whether the change is a supported package update or a separate migration.
  2. Record installed and available 389-ds-base versions and list every instance with dsctl -l.
  3. Save timestamped baselines for packages, per-instance health or status evidence, entry counts, and replication.
  4. Create and verify a native backup for every production instance on the host.
  5. Preview and install dnf update 389-ds-base on one host at a time, including cockpit-389-ds when installed.
  6. In replicated topologies, update hosts one at a time and confirm synchronization before continuing.
  7. Reload the saved baseline, compare post-upgrade evidence, and validate LDAP, TLS, plug-ins, and replication for every affected instance.
  8. Use verified backup restore or replica reinitialization when rollback requires it.

Routine package updates are straightforward when preparation and verification are disciplined. Major product, platform, server, and database-backend changes require separate migration procedures.


References


Frequently Asked Questions

1. What is the difference between updating and migrating 389 Directory Server?

A routine update installs newer 389-ds-base RPMs from your supported repository on the same host and instance layout. A migration moves data or configuration across a major product generation, operating-system major release, database backend, or new server and requires a separate procedure.

2. Does dnf update 389-ds-base restart Directory Server?

On Red Hat Directory Server and equivalent RHEL-family RPM deployments, updating 389-ds-base restarts dirsrv services for every instance on that host. On other packaging systems, confirm the package restart behavior and verify every instance after the update.

3. Can I upgrade every replica at the same time?

No. Update one host at a time, verify replication and client connectivity, then continue. Updating every supplier simultaneously removes your rollback path and can interrupt directory service for all clients.

4. Is dnf downgrade a safe rollback after a failed upgrade?

Not by itself. Downgrading the package does not undo configuration, schema, plugin, or database changes the newer version may have applied. Use a verified pre-upgrade backup or rebuild the replica from a healthy supplier instead.

5. Do I need a separate Directory Server upgrade command after dnf update?

No for supported in-place package updates. Installing the newer 389-ds-base RPMs is the upgrade step. Major generation changes are handled through migration workflows documented separately by the vendor.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …