Update 2022-10-10: I published the code and added more container versions on dockerhub. Check the AnsibleDS post update.

Recently, I was massively bored over the weekend. So I started to redo my personal server setup.

I wanted to use an LDAP server for user management and got instantly confused over which one to choose. OpenLDAP is said to be fast and reliable, but the documentation is all over the place and usability is limited at best. 389 Directory Server has a better documentation but you need an extra management server and a console server. That was just too much hassle for me.

Luckily, there is ApacheDS. Sure, its written in Java but you only need to run one process and it comes with a fully fledged IDE.

Then again, there is no official docker container. So I made my own, together with a set of ansible scripts to deploy the container to my server. Enjoy!


You need a server that is accessible via ssh. In creating this post I used a Ubuntu Server 20.04 with hostname tnglab. You should be able to log in on port 22. My local machine was a MacOS with Python 3.8 and installed ansible package. You can install the ansible package in several ways, as the ansible documentation descibes.


We will create an ansible setup that will install Docker on tnglab and then run the ApacheDS docker image.

Set up Ansible

In a new directory we create the hosts.yml, into which we enter the access information for tnglab.

hosts.ymlview raw
ansible_host: tnglab.fritz.box
ansible_port: 22
ansible_user: root
ansible_ssh_private_key_file: ~/.ssh/id_rsa_asterix

We list the hosts.yml in the additional Ansible configuration file ansible.cfg so that we don’t have to enter the hosts.yml path to every Ansible command.

ansible.cfgview raw
inventory = hosts.yml

After that, the following command should result result below.

benjamin@asterix:example# ansible -m ping tnglab
tnglab | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
"changed": false,
"ping": "pong"

Our basic Ansible setup is now working, now we need a Docker image to start.

ApacheDS Docker Image

There are already a few ApacheDS docker images available on Docker Hub but at the time that I wrote this blog post, there wasn’t one available that would also provide the code for creating it. As the LDAP server can be a very vital and security relevant part of any ones infrastructure, I created my own image.

I will provice the code necessary below. This is not a complete example on how to create a docker image. If you are just interested in using the image, you can pull it from Docker Hub:

docker pull gevattergaul/apacheds

The Dockerfile copies the original config from config/config.ldif and the logging config from config/log4j.properties, as well as entrypoint.sh to the image. The config.ldif remains unchanged, I copied it from the upstream ApacheDS distribution. The logging config is adjusted to log to stdout so that we get log output in docker.

FROM alpine:3.12

RUN apk update && \
apk add openjdk11-jre bash && \
rm -rf /var/cache/apk/*

COPY ./apacheds /opt/apacheds
COPY config/config.ldif /opt/apacheds/config.ldif.default
COPY config/log4j.properties /opt/apacheds/log4j.properties.default
COPY bin/entrypoint.sh /entrypoint.sh

EXPOSE 389/tcp

CMD /entrypoint.sh
log4j.rootCategory=WARN, stdout

log4j.appender.stdout.layout.ConversionPattern=[%d{HH:mm:ss}] %p [%c] - %m%n

The files are copied to the image as *.default files. The entrypoint will move them to the correct position in the container on first start. When you restart the container, the files will not be copied again. This is necessary for both config files. This way we can mount a custom log config to the container. Also, the config.ldif may only be present when ApacheDS is first started. There, it will convert it to its internal data structure and move the file away. If the config.ldif was present on a restart, ApacheDS would throw an error.


mkdir -p /opt/apacheds/instances/default/cache
mkdir -p /opt/apacheds/instances/default/conf
mkdir -p /opt/apacheds/instances/default/log
mkdir -p /opt/apacheds/instances/default/partitions
mkdir -p /opt/apacheds/instances/default/run

# We move the config files once at most, because apacheds might delete them
# at runtime. If we copy them again, this might result in an error.
if [ -e /opt/apacheds/config.ldif.default ];
if [ ! -e /opt/apacheds/instances/default/conf/config.ldif_migrated ];
mv /opt/apacheds/config.ldif.default /opt/apacheds/instances/default/conf/config.ldif
rm /opt/apacheds/config.ldif.default

if [ ! -e /opt/apacheds/instances/default/conf/log4j.properties ] && [ -e /opt/apacheds/log4j.properties.default ];
mv /opt/apacheds/log4j.properties.default /opt/apacheds/instances/default/conf/log4j.properties

/opt/apacheds/bin/apacheds.sh run

You can build the docker image like this:

wget https://downloads.apache.org/directory/apacheds/dist/2.0.0.AM26/apacheds-2.0.0.AM26.tar.gz
tar -xzf apacheds-2.0.0.AM26.tar.gz
mv apacheds-2.0.0.AM26 apacheds
rm -rf apacheds/instances
mkdir apacheds/instances
docker build -t apacheds:latest .

Now that we have a docker image, we shall deploy it to tnglab.

Deploy ApacheDS

In the ansible directory from before we now create the config files for the LDAP role. Roles are one possibility to reuse code in Ansible. The Ansible wiki describes roles in detail.

The role loads the image from Docker Hub, mounts the ApacheDS runtime files to the local harddrive of tnglab and opens the LDAP port on tnglab. The LDAP port will only be accessible on tnglab. Therefore we can use the role on public machines without the risk that someone will access our unsecured LDAP server. Later, we will use a ssh tunnel to access the LDAP port on tnglab.

roles/ldap/tasks/main.ymlview raw
- name: Ensure apacheds container
name: apacheds
image: gevattergaul/apacheds:latest
- ""
- "{{ apacheds_data_directory }}:/opt/apacheds/instances"

If you haven’t installed docker on your host yet, you can use the following role.

roles/dockerhost/tasks/main.ymlview raw
- name: Add docker
- docker.io
- python3-docker
state: present

We combine both in a playbook demo.yml.

demo.ymlview raw
- hosts: tnglab
- dockerhost
- ldap

Including the files from before, the directory structure should now look like this.

|-- ansible.cfg
|-- demo.yml
|-- hosts.yml
`-- roles
|-- dockerhost
| `-- tasks
| `-- main.yml
`-- ldap
`-- tasks
`-- main.yml

So we can run it by invoking the playbook.

ansible-playbook demo.yml

Now we should have a running ApacheDS instance in a Docker container on tnglab.

Access ApacheDS Instance

To access the ApacheDS instance, we have to access the LDAP port on tnglab which is not publicly available. Therefore, we use a ssh tunnel to make the port available on our local machine.

ssh -L 10389:localhost:389 tnglab

Now you can access the ApacheDS instance over your localhost on port 389. In Apache Directory Studio you can enter the connection information like this:

If you have started ApacheDS in its standard configuration as descibed above, you can log in with the admin user uid=admin,ou=system and the default password secret:

Now you should have full access to the ApacheDS instance. Changes to the LDAP tree will be saved on tnglab and persisted on restarts of the ApacheDS container. Don’t forget to change your password.