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?
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