Update TLS certificate with Reverse Proxy automatictly

Your question

Hi, I’ve installed my Stalwart environment as a Docker container behind a reverse proxy, but now Stalwart itself needs a TLS certificate. Currently, I’m adding it manually and then restarting the container. Is there a way to automate this? Does anyone have a workflow for this?

There is some documentation here:

What I like best is the systemd service that checks whenever the certificate file has changed to reload stalwart.

But that only applies if you install it directly on the host and not via Docker, because Docker can’t access the host’s directory.

You can share files from the container and the host.

And how does that work?

Assuming you are using docker volumes or bind mounts for your containers, this is how things could work. mailcerts-script runs via cron once a day. Just change the “<*-home>” entries with your values.

Note, that these are not bullet-proof fully grown scripts, just little helpers.

Hope that helps.

Cheers

Michael aka /Bytewurm

#!/bin/bash

#
# mailcerts - copy certificates from Caddy to Stalwart
#
# caddy bind-dir "acme" mounted into caddy:/data/acme
# stalwart bind-dir "certs" mounted into stalwart:/etc/mycerts
#
# referencing within stalwart as "/etc/mycerts/example1.{crt,key}", etc.
# stalwart-cli required (using docker container and wrapper script in /usr/local/bin)
#

certs_updated=0

caddy_dir="<caddy-home>/acme/certificates/acme-v02.api.letsencrypt.org-directory"
stalwart_dir="<stalwart-home>/certs"

for domain in example1.com mail.example1.com example2.com; do

  crt_file="${domain}.crt"
  md5_caddy=$(md5sum ${caddy_dir}/${domain}/${crt_file} | cut -d' ' -f1)
  md5_stalwart=$(md5sum ${stalwart_dir}/${crt_file} | cut -d' ' -f1)

  if [[ "$md5_caddy" != "$md5_stalwart" ]]; then

    cp ${caddy_dir}/${domain}/${domain}.{crt,key} ${stalwart_dir}
    let certs_updated++

  fi

done

if [[ $certs_updated -gt 0 ]]; then
  
  # fix permissions for new certs
  chown 2000:2000 ${stalwart_dir}/*.{crt,key}
  chmod 440 ${stalwart_dir}/*.{crt,key}

  # then reload them
  /usr/local/bin/stalwart-cli create action/ReloadTlsCertificates --json '{}'

fi

Place this as /usr/local/bin/stalwart-cli, adapt homedir, stalwart docker network + create file STALWART_ADMIN_TOKEN with a valid token from stalwart.

#!/bin/sh

# Wrapper for stalwart-cli container
# note the docker volume to be used as cache, replace with simple bind-mount
# or remove it - will work anyways

cmd="$*"
ADMIN_TOKEN=$(cat <stalwart_home>/STALWART_ADMIN_TOKEN)

docker run --rm --network <stalwart_docker_network> --name stalwart_cli \
  -e "STALWART_URL=http://stalwart:8080" \
  -e "STALWART_TOKEN=${ADMIN_TOKEN}" \
  -v "stalwart-cli_cache:/home/nonroot/.cache" \
  stalwartlabs/cli:1.0.10 ${cmd}

Here’s an ugly vibecoded (with minimal manual changes) solution that seems to work for me. PROS: no stalwart-cli.

I use stalwart in the container, caddy on the host, and caddy retrieves the certificates. The script (should be added to cron) copies the certificates when they change to /opt/mail/stalwart-cert, adjusts the permissions, and triggers the certificate update via the JMAP endpoint.

To adapt to your configuration, you’ll need to tweak the script - obviously, by changing example.com, the paths and the token (see here How To: reloading TLS certificates on v0.16 · stalwartlabs/stalwart · Discussion #3199 · GitHub about the token and the necessary permissions - this is what the llm took its inspiration from). And the path (in my case, /opt/mail/stalwart-cert) should be bind-mounted to the container. And you should manually add the certificates “from file” (Settings - TLS - Certificates), but this is a one-time action - the subsequent renewals will be performed via the script.

#!/bin/bash

# --- CONFIGURATION ---
CADDY_CERT_PATH="/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.com"
STALWART_CERT_DIR="/opt/mail/stalwart-cert"

# Your Stalwart Internal JMAP endpoint and the Account API key
STALWART_JMAP_URL="https://mail.example.com/jmap"
STALWART_TOKEN="API_******************************"
# ---------------------

# 1. Ensure the clean destination directory exists
mkdir -p "$STALWART_CERT_DIR"

# 2. Synchronize files safely from Caddy's folder only if they have changed
cp -ur "$CADDY_CERT_PATH" "$STALWART_CERT_DIR"

# 3. Forcibly adjust the file permissions to give Stalwart's UID 2000 access
chown -R 2000:2000 "$STALWART_CERT_DIR"
chmod 644 "$STALWART_CERT_DIR/example.com/example.com.crt"
chmod 600 "$STALWART_CERT_DIR/example.com/example.com.key"

# 4. Trigger the official v0.16 JMAP method block to instantly reload the files into memory
RESPONSE=$(curl -s -L -w "%{http_code}" -X POST "$STALWART_JMAP_URL" \
  -H "Authorization: Bearer $STALWART_TOKEN" \
  -H "Content-Type: application/json" \
  --data-raw '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:Action/set",{"create":{"action-0":{"@type":"ReloadTlsCertificates"}}},"0"]]}')

# 5. Handle logging error outputs
HTTP_CODE="${RESPONSE:${#RESPONSE}-3}"

if [ "$HTTP_CODE" -eq 200 ]; then
    echo "$(date): Stalwart hot-reloaded the fresh certificates seamlessly."
else
    echo "$(date): Failed to trigger JMAP reload. HTTP Status: $HTTP_CODE"
    exit 1
fi