diff --git a/docker-compose.yml b/docker-compose.yml
index 8fdac63ae9e147e49ba9a205f63fee7a64dfab98..f25b9e48ee7804968ee320d54134c5f592e4e24c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,13 +5,17 @@ services:
   ldap:
     build:
       context: slapd/
-    volumes:
-      - _run_ldap:/run/slapd
-      - ldap:/var/lib/ldap
+    domainname: "${LDAP_HOSTNAME:-ldap}.${LDAP_DOMAIN:-nowhere.com}"
     environment:
       LDAP_ROOTPASS: "${LDAP_ROOTPASS:-foo}"
       LDAP_DOMAIN: "${LDAP_DOMAIN:-nowhere.com}"
       LDAP_ORGANIZATION: "${LDAP_ORGANIZATION:-none}"
+    hostname: "${LDAP_HOSTNAME:-ldap}"
+    restart: always
+    volumes:
+      - _run_slapd:/run/slapd
+      - ldap:/var/lib/ldap
+      - backup_ldap:/var/backups/ldap
 
   nss-pam-ldapd:
     build:
@@ -20,23 +24,29 @@ services:
     environment:
       LDAP_BASE_DN: "${LDAP_BASE_DN:-dc=nowhere,dc=com}"
     volumes:
-      - _run_ldap:/run/slapd
+      - _run_slapd:/run/slapd
 
   ldap-account-manager:
     build:
       context: ldap-account-manager/
     links:
       - ldap
-    volumes:
-      - _run_ldap:/run/slapd
-      - ldap-account-manager:/var/lib/ldap-account-manager
     ports:
       - 80:80
+    restart: always
+    volumes:
+      - _run_slapd:/run/slapd
+      - ldap-account-manager:/var/lib/ldap-account-manager
 
 volumes:
-  _run_ldap:
+  _run_slapd:
   ldap:
   ldap-account-manager:
+    labels:
+      snapshot: 'true'
+  backup_ldap:
+    labels:
+      snapshot: 'true'
 
 networks:
   default:
diff --git a/slapd/Dockerfile b/slapd/Dockerfile
index 8b3b550ca400dae3f3c75cb47f44240e17f70cd3..956aeffbfa138972ea2b49acb151e1d9b115e32d 100644
--- a/slapd/Dockerfile
+++ b/slapd/Dockerfile
@@ -1,19 +1,32 @@
-FROM debian:stretch-slim
-RUN echo 'deb http://deb.debian.org/debian stretch-backports main' > /etc/apt/sources.list.d/backports.list && \
-    apt-get update && \
+FROM debian:buster-slim
+# hadolint ignore=DL3008
+RUN apt-get update && \
     DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
-        gnutls-bin=3.5.8-5+deb9u4 \
-        ldap-utils=2.4.44+dfsg-5+deb9u2 \
-        slapd=2.4.44+dfsg-5+deb9u2 \
+        gettext-base \
+        gnutls-bin \
+        ldap-utils \
+        slapd \
+        ssl-cert \
+        time \
     && \
-    mkdir -p /run/slapd && \
-    rm -rf /tmp/* /var/tmp/* /var/lib/apt/lists/* /var/cache/apt/archives/*
-COPY --chown=root:root entrypoint /
+    usermod -aG ssl-cert openldap && \
+    rm -rf /tmp/* /var/tmp/* /var/cache/apt/archives/* /var/lib/apt/lists/* && \
+    rm -rf /etc/ssl/certs/ssl-cert-snakeoil.pem /etc/ssl/private/ssl-cert-snakeoil.key && \
+    rm -rf /var/lib/ldap/* /var/backups/ldap/* /run/slapd/* /etc/ldap/slapd.d
+COPY --chown=root:root config.ldif /usr/share/slapd/
+COPY --chown=root:root skel.ldif /usr/share/slapd/
+COPY --chown=root:root entrypoint /usr/local/sbin/
+COPY --chown=root:root backup /usr/local/sbin/
 EXPOSE 389 636
 VOLUME [ "/var/lib/ldap" ]
-VOLUME [ "/run/ldap" ]
+VOLUME [ "/run/slapd" ]
+VOLUME [ "/var/backups/ldap" ]
 ENV LDAP_URLS="ldap:/// ldapi:/// ldaps:///" \
-    SLAPD_DEBUG_LEVEL="NONE"
-ENTRYPOINT [ "/entrypoint" ]
-CMD [ "slapd", "-F", "/etc/ldap/slapd.d", "-u", "openldap", "-g", "openldap", "-h", "\"$LDAP_URLS\"", "-d", "$SLAPD_DEBUG_LEVEL" ]
-HEALTHCHECK CMD ldapsearch -b cn=config -H ldapi:/// > /dev/null || exit 1
+    SLAPD_DEBUG_LEVEL="stats,stats2,none" \
+    SSL_CERT_FILE="/etc/ssl/certs/ssl-cert-snakeoil.pem" \
+    SSL_KEY_FILE="/etc/ssl/private/ssl-cert-snakeoil.key" \
+    SSL_CA_FILE="/etc/ssl/certs/ssl-cert-snakeoil.pem"
+ENTRYPOINT [ "entrypoint" ]
+CMD [ "slapd", "-F", "/var/lib/ldap/config", "-u", "openldap", "-g", "openldap", "-h", "\"$LDAP_URLS\"", "-d", "$SLAPD_DEBUG_LEVEL" ]
+HEALTHCHECK CMD ldapsearch -b cn=config > /dev/null || exit 1
+STOPSIGNAL INT
diff --git a/slapd/README.md b/slapd/README.md
index 0b4799bde22bcb20ea729a751d79bea64cdd5371..14f5eab6aa48835a1880278cb1e4b5e7ffe0d887 100644
--- a/slapd/README.md
+++ b/slapd/README.md
@@ -15,10 +15,27 @@ Name | Description | Default value
 `LDAP_ROOTPASS` | Root password.
 `LDAP_DOMAIN` | Domain.
 `LDAP_ORGANIZATION` | Organization.
+`SLAPD_DEBUG_LEVEL` | The `slapd` debug/ log level.
+`SSL_CERT_FILE` | Location of the SSL certificate file.
+`SSL_KEY_FILE` | Location of the SSL key file.
+`SSL_CA_FILE` | Location of the SSL certificate authority file.
+
+## SSL
+
+If the relevant environment variables aren't changed from their default values,
+on startup the container will generate a key and self-signed certificate with
+the FQDN of the container. If the location of the SSL key and certificate are
+provided, those are used instead.
 
 ## Persistence
 
-The database is at `/var/lib/ldap`.
+The configuration (`cn=config`) and data LDAP directories reside in the
+`config` and `data` diretories respectively in the `/var/lib/ldap` volume.
+The LDAP directories are generated only if they're missing. Changes to
+environment variables afterwards won't change the configuration, since that is
+persisted to a volume. There's also the `/var/backups/ldap` volume where the
+`backup` script saves snapshots of the LDAP directories (config directory
+included).
 
 ## License
 
diff --git a/slapd/backup b/slapd/backup
new file mode 100755
index 0000000000000000000000000000000000000000..6ba3110ff0b866dbea711b51333110ffefba07b4
--- /dev/null
+++ b/slapd/backup
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eux
+
+slapcat -n0 -v -l /var/backups/ldap/config.ldif
+
+for dn in $(ldapsearch -Y EXTERNAL -LLL -s base -b '' o namingContexts | sed -n '/namingContexts/ s/namingContexts: //gp')
+do
+    slapcat -b "$dn" -v -l "/var/backups/$dn.ldif"
+done
diff --git a/slapd/config.ldif b/slapd/config.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..433d8261f04baf6af475af5df6333eef95a0af94
--- /dev/null
+++ b/slapd/config.ldif
@@ -0,0 +1,108 @@
+# Global config:
+dn: cn=config
+objectClass: olcGlobal
+cn: config
+# Where the pid file is put. The init.d script
+# will not stop the server if you change this.
+olcPidFile: /var/run/slapd/slapd.pid
+# List of arguments that were passed to the server
+olcArgsFile: /var/run/slapd/slapd.args
+# Read slapd-config(5) for possible values
+olcLogLevel: none
+# The tool-threads parameter sets the actual amount of cpu's that is used
+# for indexing.
+olcToolThreads: 1
+# TLS options.
+olcTLSCACertificateFile: ${SSL_CA_FILE}
+olcTLSCertificateFile: ${SSL_CERT_FILE}
+olcTLSCertificateKeyFile: ${SSL_KEY_FILE}
+olcTLSCipherSuite: SECURE256:+SECURE128
+olcTLSProtocolMin: 3.1
+olcTLSDHParamFile: /usr/share/slapd/dh.pem
+
+# Frontend settings
+dn: olcDatabase={-1}frontend,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcFrontendConfig
+olcDatabase: {-1}frontend
+# The maximum number of entries that is returned for a search operation
+olcSizeLimit: 500
+# Allow unlimited access to local connection from the local root user
+olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
+# Allow unauthenticated read access for schema and base DN autodiscovery
+olcAccess: {1}to dn.exact="" by * read
+olcAccess: {2}to dn.base="cn=Subschema" by * read
+
+# Config db settings
+dn: olcDatabase=config,cn=config
+objectClass: olcDatabaseConfig
+olcDatabase: config
+# Allow unlimited access to local connection from the local root user
+olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
+olcRootDN: cn=admin,cn=config
+
+# Load schemas
+dn: cn=schema,cn=config
+objectClass: olcSchemaConfig
+cn: schema
+
+include: file:///etc/ldap/schema/core.ldif
+include: file:///etc/ldap/schema/cosine.ldif
+include: file:///etc/ldap/schema/nis.ldif
+include: file:///etc/ldap/schema/inetorgperson.ldif
+
+# Load module
+dn: cn=module{0},cn=config
+objectClass: olcModuleList
+cn: module{0}
+# Where the dynamically loaded modules are stored
+olcModulePath: /usr/lib/ldap
+olcModuleLoad: back_mdb
+
+# Set defaults for the backend
+dn: olcBackend=mdb,cn=config
+objectClass: olcBackendConfig
+olcBackend: mdb
+
+# The database definition.
+dn: olcDatabase=mdb,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcMdbConfig
+olcDatabase: mdb
+# Checkpoint the database periodically in case of system
+# failure and to speed slapd shutdown.
+olcDbCheckpoint: 512 30
+olcDbMaxSize: 1073741824
+# Save the time that the entry gets modified, for database #1
+olcLastMod: TRUE
+# The base of your directory in database #1
+olcSuffix: ${BASE_DN}
+# Where the database file are physically stored for database #1
+olcDbDirectory: /var/lib/ldap/data
+# olcRootDN directive for specifying a superuser on the database. This
+# is needed for syncrepl.
+olcRootDN: cn=admin,${BASE_DN}
+olcRootPW: ${PASSWORD_HASH}
+# Indexing options for database #1
+olcDbIndex: objectClass eq
+olcDbIndex: cn,uid eq
+olcDbIndex: uidNumber,gidNumber eq
+olcDbIndex: member,memberUid eq
+# The userPassword by default can be changed by the entry owning it if
+# they are authenticated. Others should not be able to see it, except
+# the admin entry above.
+olcAccess: to attrs=userPassword
+  by self write
+  by anonymous auth
+  by * none
+# Allow update of authenticated user's shadowLastChange attribute.
+# Updating it on password change is implemented at least by libpam-ldap,
+# libpam-ldapd, and the slapo-smbk5pwd overlay.
+olcAccess: to attrs=shadowLastChange
+  by self write
+  by * read
+# The admin dn (olcRootDN) bypasses ACLs and so has total access,
+# everyone else can read everything.
+olcAccess: to *
+  by * read
+
diff --git a/slapd/entrypoint b/slapd/entrypoint
index a1ea36e31077d7f86bd240aa0250da9fa11d7a20..2317c15b8e4a88144998534205d5de69ab8c2099 100755
--- a/slapd/entrypoint
+++ b/slapd/entrypoint
@@ -1,18 +1,70 @@
 #!/bin/sh
 set -eux
 
+# Get the root password hash and unset the cleartext password.
+PASSWORD_HASH="$(slappasswd -ns "$LDAP_ROOTPASS")"
+unset LDAP_ROOTPASS
+export PASSWORD_HASH
+
+# See https://github.com/moby/moby/issues/8231
+# shellcheck disable=SC2039
+ulimit -n 1024 || true
+
+# Create and set owner for runtime directories.
 install -d -o openldap -g openldap /run/slapd
+install -d -o openldap -g openldap /var/backups/ldap
 install -d -o openldap -g openldap /var/lib/ldap
+install -d -o openldap -g openldap /var/lib/ldap/config
+install -d -o openldap -g openldap /var/lib/ldap/data
+
+# Base DN.
+BASE_DN="dc=$(echo "$LDAP_DOMAIN" | sed 's/^\.//; s/\.$//; s/\./,dc=/g')"
+export BASE_DN
+
+# DC.
+DC="$(echo "$LDAP_DOMAIN" | sed 's/^\.//; s/\..*$//')"
+export DC
+
+# Generate self-signed certificates if none are provided.
+if [ "${SSL_CERT_FILE:-}" = "/etc/ssl/certs/ssl-cert-snakeoil.pem" ] || \
+   [ "${SSL_KEY_FILE:-}" = "/etc/ssl/private/ssl-cert-snakeoil.key" ]
+then
+    echo Generating self-signed key and certificate. >&2
+    DEBIAN_FRONTEND=noninteractive time make-ssl-cert generate-default-snakeoil --force-overwrite
+fi
+
+# Generate random DH parameters.
+echo Generating DH parameters, this will take a while. >&2
+time openssl dhparam -out /usr/share/slapd/dh.pem 2048
+
+# Run slapadd with the correct user and location of the config directory.
+alias slapadd='chroot --userspec openldap:openldap / slapadd -gv -F /var/lib/ldap/config'
+
+# Create configuration is none is present.
+if [ -z "$(find /var/lib/ldap/config -maxdepth 1 -mindepth  1)" ]
+then
+    echo No configuration found, generating a new one. >&2
+    # shellcheck disable=SC2002
+    cat /usr/share/slapd/config.ldif | envsubst | slapadd -b 'cn=config'
+fi
+
+# Create directory if none is present.
+if [ -z "$(find /var/lib/ldap/data -maxdepth 1 -mindepth 1)" ]
+then
+    echo No directory found, generating a new one, >&2
+    # shellcheck disable=SC2002
+    cat /usr/share/slapd/skel.ldif | envsubst | slapadd -b "$BASE_DN"
+fi
 
-cat << EOF | debconf-set-selections -v
-slapd slapd/internal/generated_adminpw password ${LDAP_ROOTPASS:-}
-slapd slapd/internal/adminpw password ${LDAP_ROOTPASS:-}
-slapd slapd/password2 password ${LDAP_ROOTPASS:-}
-slapd slapd/password1 password ${LDAP_ROOTPASS:-}
-slapd slapd/domain string ${LDAP_DOMAIN:-}
-slapd shared/organization string ${LDAP_ORGANIZATION:-}
+# Configure the client.
+cat << EOF >> /etc/ldap/ldap.conf
+URI         ldapi:///
+SASL_MECH   EXTERNAL
+BASE        $BASE_DN
 EOF
 
-DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive slapd
+# Unset the root password hash.
+unset PASSWORD_HASH
 
+# Run CMD.
 eval exec "$@"
diff --git a/slapd/skel.ldif b/slapd/skel.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..fedc1898cd476a73e64dedade459f5b288702a7d
--- /dev/null
+++ b/slapd/skel.ldif
@@ -0,0 +1,13 @@
+dn: ${BASE_DN}
+objectClass: top
+objectClass: dcObject
+objectClass: organization
+o: ${LDAP_ORGANIZAION}
+dc: ${DC}
+
+dn: cn=admin,${BASE_DN}
+objectClass: simpleSecurityObject
+objectClass: organizationalRole
+cn: admin
+description: LDAP administrator
+userPassword: ${PASSWORD_HASH}