2026.07 / MAIL TLS 032
Docker Mailserver Still Serving an Old Let’s Encrypt Certificate?
Certbot says renewal succeeded, the certificate file on the host has a new expiry date, but a mail client still warns that Docker Mailserver is presenting the old certificate. This is usually not a certificate-authority problem. It is a handoff problem between four separate layers: the ACME client, the host filesystem, the container mount, and the long-running Postfix or Dovecot process accepting the connection.
The fastest way to fix it is to compare the same certificate at each layer instead of repeatedly requesting another one. The examples below use mail.example.com, a generic Compose service called mailserver, and placeholder paths. Match them to your own deployment without printing private keys, DNS credentials, account data, or an unredacted Compose environment.
A successful renewal only proves that new certificate files were written. It does not prove the container can see them or that SMTP and IMAP have loaded them.
The certificate path from issuer to client
A healthy renewal has to complete this whole chain:
ACME renewal succeeds
→ renewed files exist on the host
→ the full certificate directory is mounted into the container
→ Docker Mailserver selects the intended mail FQDN
→ Postfix and Dovecot reload the files
→ public SMTP and IMAP handshakes serve the new leaf certificate
Checking only one arrow creates false confidence. A browser test is not useful here either: the HTTPS certificate on a web proxy may be completely separate from the certificate served by SMTP submission or IMAPS.
1. Record what the public mail ports actually serve
Start outside the container. Query the exact hostname and protocol used by clients:
# IMAPS: TLS begins immediately
openssl s_client -connect mail.example.com:993 \
-servername mail.example.com -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -serial -dates -fingerprint -sha256
# SMTP submission: upgrade with STARTTLS
openssl s_client -starttls smtp -connect mail.example.com:587 \
-servername mail.example.com -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -serial -dates -fingerprint -sha256
Test both protocols. Postfix and Dovecot are separate services and can disagree if only one reloaded. The leaf certificate should cover the hostname clients connect to; mailbox domains do not each need their own mail-server certificate when they all use one shared mail FQDN.
Keep the serial number, SHA-256 fingerprint, and notAfter date for comparison. Do not use the expiry date alone: two different certificates can share a similar validity window.
2. Check Certbot’s selected lineage on the host
Ask Certbot which certificate lineage it manages, then inspect the leaf certificate without displaying private-key contents:
sudo certbot certificates
sudo openssl x509 \
-in /etc/letsencrypt/live/mail.example.com/fullchain.pem \
-noout -subject -issuer -serial -dates -fingerprint -sha256
sudo readlink -f /etc/letsencrypt/live/mail.example.com/fullchain.pem
If the host fingerprint is still old, the problem is upstream of Docker. Review the renewal log and the certificate name Certbot actually renewed. Duplicate lineages such as a suffixed certificate name can leave the mail container pointed at an older directory even though Certbot renewed a different one.
Do not force repeated production renewals as a diagnostic step. They can consume certificate-authority rate limits while leaving a bad mount or reload untouched. Use Certbot’s dry run to test renewal configuration:
sudo certbot renew --dry-run
Let’s Encrypt no longer sends certificate-expiration notification emails, so working automation and independent monitoring matter more than inbox reminders. Its announced move toward shorter certificate lifetimes is another reason to avoid calendar-based manual renewal.
3. Verify the complete certificate tree is mounted
Let’s Encrypt’s live/ files are normally symlinks into archive/. Mounting only one symlink or only the live/ directory can produce a broken or frozen view inside the container. Docker Mailserver’s TLS documentation shows mounting the certificate directory so both sides of those links remain available.
A generic Compose shape is:
services:
mailserver:
image: mailserver/docker-mailserver:<pinned-version>
hostname: mail.example.com
environment:
SSL_TYPE: letsencrypt
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
Render the actual Compose configuration before changing it, but avoid sharing output that contains secrets:
docker compose config
docker inspect mailserver \
--format '{{range .Mounts}}{{println .Source " -> " .Destination}}{{end}}'
Then compare the certificate from inside the running container:
docker compose exec mailserver openssl x509 \
-in /etc/letsencrypt/live/mail.example.com/fullchain.pem \
-noout -serial -dates -fingerprint -sha256
# Confirm the symlink target resolves inside the container
docker compose exec mailserver readlink -f \
/etc/letsencrypt/live/mail.example.com/fullchain.pem
If the host is new and the container is old, inspect the mount first. If the path is absent, the Compose source path, destination path, certificate name, or symlink target is wrong. Recreating certificates will not repair any of those.
4. Reload only after the files agree
Once the host and in-container fingerprints match, make the running mail services load the renewed certificate. A controlled Compose restart is simple and reliable for a small installation:
docker compose restart mailserver
docker compose ps
docker compose logs --since 5m mailserver
A restart causes a short interruption, so schedule it sensibly and watch health and startup logs. Do not run docker compose down -v; removing volumes is unrelated to TLS and risks persisted mail data. If your pinned Docker Mailserver version documents a supported service reload procedure, that can reduce interruption, but verify both Postfix and Dovecot rather than assuming one signal refreshed everything.
Immediately repeat the public tests on ports 993 and 587. The public fingerprint—not the file timestamp—is the acceptance test.
5. Use a deploy hook, not an unconditional restart loop
Certbot’s official user guide supports deploy hooks that run only after a successful renewal. That is the right place to reload or restart the mail service. A daily command that restarts Docker Mailserver regardless of whether anything renewed adds needless disruption and can hide a failing renewal behind a healthy restart.
Keep the hook small, root-owned, and non-secret. Its job is only to make the already renewed files active:
#!/bin/sh
set -eu
cd /srv/example-mail-stack
/usr/bin/docker compose restart mailserver
Store this as a Certbot renewal deploy hook according to your Certbot installation method. Use a generic stack path in public documentation, keep permissions restrictive, and do not embed a Cloudflare token, mailbox password, webhook, or private-key data in the script. Some containerised Certbot setups need a different handoff because the Certbot container cannot control the host Docker daemon; in that case, keep renewal and host-side reload as separate, least-privilege steps.
6. Test the automation without waiting for expiry
A useful test proves more than “the timer exists”:
- Run
certbot renew --dry-runand confirm the simulated ACME challenge succeeds. - Review the systemd timer, cron entry, or container schedule that launches renewal.
- Confirm the deploy hook’s working directory and Compose service name are correct.
- Run the hook manually during a maintenance window.
- Check container health and recent logs.
- Query public IMAPS and STARTTLS and record their fingerprints and expiry dates.
A dry run may not exercise deploy hooks by default on every Certbot version and invocation. Read the output and the documentation for your installed version rather than inferring that a simulated renewal also restarted Docker Mailserver.
Common reasons the old certificate survives
- Wrong certificate lineage: Certbot renewed a similarly named certificate directory while DMS still reads the original.
- Partial mount:
live/is mounted but its relative symlinks intoarchive/cannot resolve. - Stale bind source: Compose mounts a copied certificate directory that Certbot never updates.
- No post-renewal reload: the new file is visible, but Postfix or Dovecot still holds the old certificate in memory.
- Only one protocol tested: IMAP is current while SMTP submission is stale, or vice versa.
- Wrong public destination: DNS, NAT, or a second mail host sends the test to another machine.
- Hostname mismatch: the certificate is valid but does not cover the FQDN configured in clients and DMS.
- Web TLS confused with mail TLS: a reverse proxy’s HTTPS certificate is fresh while the mail ports are not.
A compact renewal acceptance checklist
- Certbot reports the expected certificate name and future expiry.
- The host certificate has the expected SAN, serial, and fingerprint.
- The same fingerprint is visible at the configured path inside the container.
- The full
live/andarchive/relationship resolves through the read-only mount. - Docker Mailserver is restarted or fully reloaded only after successful renewal.
- Container health and logs show a clean return to service.
- Public IMAPS and SMTP STARTTLS serve the new fingerprint and complete chain.
- A real client can authenticate and send a harmless test message.
- An external monitor will warn before expiry without relying on Let’s Encrypt email.
The official Docker Mailserver TLS documentation is the version-sensitive source for certificate layouts and SSL_TYPE. Certbot’s renewal documentation explains dry runs and renewal hooks. Use those references for the versions you actually run, then trust the live SMTP and IMAP handshakes as the final proof.