I’m sharing this guide because I spent a lot of time troubleshooting SSL errors while setting up Poste.io behind Dokploy. If you are using Dokploy, you likely noticed that the Let's Encrypt certificates managed by Traefik work perfectly for the web dashboard but aren't automatically applied to the mail server ports (25, 587, 993, 995, etc.). This leads to "SSL/TLS Handshake" errors in external clients like Gmail or Outlook.
When trying to generate certificates inside Poste.io itself using the built-in Let's Encrypt client, I constantly hit this error:
LEScript.ERROR: 400 ... Unable to update challenge :: authorization must be pending
Instead of forcing Poste.io to get its own certificate, this method "feeds" Dokploy's existing certificates into the Poste.io container. This ensures that the Full Chain (Leaf + Intermediate certificates) is present, which is required for mail clients to verify the server's identity.
Note: If you plan to use Method 1 (Schedules), you must include the /etc/dokploy/traefik/ volume mapping shown below and make sure your DOMAIN=${DOMAIN} environment variable is setup.
version: "3.8"
services:
mailserver:
image: analogic/poste.io:latest
container_name: mailserver
restart: unless-stopped
hostname: ${DOMAIN}
ports:
- "25:25" # SMTP
- "110:110" # POP3
- "143:143" # IMAP
- "465:465" # SMTPS
- "587:587" # Submission
- "993:993" # IMAPS
- "995:995" # POP3S
- "4190:4190" # Sieve
environment:
- TZ=${TZ}
- DOMAIN=${DOMAIN} # Required for the sync script to read the domain ( REQUIRED FOR METHOD 1 )
- HTTPS=OFF # We let Dokploy/Traefik handle SSL for the Web UI
- HTTP_PORT=8080
volumes:
- /etc/localtime:/etc/localtime:ro
- poste-data:/data
# REQUIRED FOR METHOD 1 (Internal Sync):
- /etc/dokploy/traefik/:/data/traefik:ro
volumes:
poste-data: {}This approach, suggested by scardonadev, is great because it keeps everything within the Dokploy UI. It runs the sync script inside the running container.
In Dokploy → Poste.io container → Schedules:
- Type: Bash
- Schedule: Every Sunday at midnight (0 0 * * 0)
- Command:
set -e;
: "--- CONFIGURATION ---";
: "Path to Traefik ACME file (mapped via volume in Compose)";
ACME_FILE="/data/traefik/dynamic/acme.json";
: "Internal Poste.io SSL directory";
POSTE_SSL_DIR="/data/ssl";
: "Safety check to ensure the DOMAIN env variable exists";
echo "Started Certificate sync for $DOMAIN";
if [ -z "$DOMAIN" ]; then
echo "Error: DOMAIN environment variable is not set!";
exit 1;
fi;
: "Check if jq is installed, if not install it";
echo "Checking if jq is installed...";
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found. Installing...";
apt-get update -qq && apt-get install -y jq;
fi;
: "Create the SSL Dir";
mkdir -p "$POSTE_SSL_DIR";
: "1. Extract the full certificate block from JSON and decode from Base64";
FULL_CERT=$(jq -r ".letsencrypt.Certificates[] | select(.domain.main==\"$DOMAIN\") | .certificate" "$ACME_FILE" | base64 -d);
: "2. Extract the private key";
PRIVATE_KEY=$(jq -r ".letsencrypt.Certificates[] | select(.domain.main==\"$DOMAIN\") | .key" "$ACME_FILE" | base64 -d);
if [ -z "$FULL_CERT" ] || [ -z "$PRIVATE_KEY" ]; then
echo "Certificate for $DOMAIN not found in acme.json";
exit 1;
fi;
: "3. Save the first certificate block as server.crt (The Leaf/Entity cert)";
echo "$FULL_CERT" | awk "/BEGIN CERTIFICATE/{i++}i==1" > "$POSTE_SSL_DIR/server.crt";
: "4. Save the remaining blocks as ca.crt (The Intermediate/CA Chain)";
echo "$FULL_CERT" | awk "/BEGIN CERTIFICATE/{i++}i>1" > "$POSTE_SSL_DIR/ca.crt";
: "5. Save the private key";
echo "$PRIVATE_KEY" > "$POSTE_SSL_DIR/server.key";
: "6. Set strict permissions (Required by Haraka/Dovecot)";
chmod 600 "$POSTE_SSL_DIR/server.crt" "$POSTE_SSL_DIR/server.key" "$POSTE_SSL_DIR/ca.crt";
: "7. Reload Dovecot to apply changes without restarting the whole container";
dovecot reload || true;
echo "$(date): Certificates synced and reloaded for $DOMAIN";Use this if you prefer managing automation via a standard .sh file on your VPS and a system cronjob.
Prerequisite: Ensure jq is installed on your VPS (sudo apt install jq).
The Sync Script (sync-mail-certs.sh):
#!/bin/bash
# --- CONFIGURATION ---
# Path to Dokploy's Traefik ACME file
ACME_FILE="/etc/dokploy/traefik/dynamic/acme.json"
# Path to your Poste.io data volume
# Note: "posteio-container_poste-data" = ID combined with volume name
POSTE_SSL_DIR="/var/lib/docker/volumes/posteio-container_poste-data/_data/ssl"
# The domain used for your mail server
DOMAIN="your.domain.com"
# ---------------------
# 1. Extract the full certificate block from JSON and decode from Base64
FULL_CERT=$(jq -r ".letsencrypt.Certificates[] | select(.domain.main==\"$DOMAIN\") | .certificate" $ACME_FILE | base64 -d)
# 2. Extract the private key
PRIVATE_KEY=$(jq -r ".letsencrypt.Certificates[] | select(.domain.main==\"$DOMAIN\") | .key" $ACME_FILE | base64 -d)
# 3. Save the first certificate block as server.crt (The Leaf/Entity cert)
echo "$FULL_CERT" | awk '/BEGIN CERTIFICATE/{i++}i==1' > $POSTE_SSL_DIR/server.crt
# 4. Save the remaining blocks as ca.crt (The Intermediate/CA Chain)
# Mail clients need this to find the path to the Trusted Root
echo "$FULL_CERT" | awk '/BEGIN CERTIFICATE/{i++}i>1' > $POSTE_SSL_DIR/ca.crt
# 5. Save the private key
echo "$PRIVATE_KEY" > $POSTE_SSL_DIR/server.key
# 6. Set strict permissions (Required by Haraka/Dovecot)
chmod 600 $POSTE_SSL_DIR/server.crt $POSTE_SSL_DIR/server.key $POSTE_SSL_DIR/ca.crt
# 7. Restart the container to apply changes
docker restart mailserver
echo "$(date): Certificates synced and mailserver restarted."Automation via Cron:
Run crontab -e and add:
# Sync certificates every Sunday at midnight
0 0 * * 0 /bin/bash /path/to/your/sync-mail-certs.sh >> /var/log/cert-sync.log 2>&1Let's Encrypt certificates are valid for 90 days, and Dokploy/Traefik typically renews them at the 60-day mark. By syncing once a week, you ensure that your mail server (Dovecot/Haraka) updates its local files long before the old certificate expires.
Important: Once you've run the sync for the first time, go to the Poste.io Admin Panel and ensure the internal Let's Encrypt setting is Disabled. This is required so Poste.io relies exclusively on the local files we just synced.
Hi @WebLenn,
thank you so much for your solution. It was honestly the only thing that worked after days and days of trying different approaches — including setting up an internal Nginx and forwarding traffic from Traefik just to make the HTTP-01 challenge work.
Your reverse synchronization approach turned out to be the cleanest and most reliable solution by far.
I took the liberty of sharing the implementation I ended up using. My goal was to have everything configured from inside the Poste.io container itself, without requiring manual cron jobs or scripts on the VPS. I documented it in this small guide:
👉 https://gist.github.com/scardonadev/7fcae8d916f9f4e68c94b2d6a73eb770
I really hope you don’t mind that I used your guide as the foundation for mine. All credit for the original idea is yours — I just wanted to help spread your work and maybe make it easier for others running Poste.io behind Traefik and Dokploy.
Thanks again for sharing such a solid solution 🙏