Bring your own TLS certificate
When to use: the host cannot use ACME (no public :80/:443, internal CA, wildcard certificate managed elsewhere).
Preconditions: a PEM certificate chain (leaf first) and its unencrypted private key, covering the mail hostname — and, since 2026-09-12, postmaster.<your domain> and webmail.<your domain>. Three names in total, whatever number of domains you host mail for: those two are one host each for the whole server. Caddy presents your certificate on all three names, so a name it omits still completes the handshake, with a certificate that does not name it: browsers and clients refuse it as a hostname mismatch. It is not replaced by a self-signed certificate. BYO mode obtains nothing from Let’s Encrypt, whichever names your certificate covers. The mail hostname keeps serving the admin panel whatever happens, so a missing postmaster. SAN costs you the new name and not your console; a missing webmail. SAN costs every user their mail client.
- At setup:
tls.mode: byowithcertificatePem/privateKeyPemin the answers file (or the wizard’s TLS step). Multi-line YAML:tls:mode: byocertificatePem: |-----BEGIN CERTIFICATE-----…privateKeyPem: |-----BEGIN PRIVATE KEY-----… - The API writes them to the
certsvolume (/certs/byo/fullchain.pem,privkey.pem0640 group 2525, whichcert-syncreads through that group).cert-syncprefers a BYO certificate over ACME, copies it to/certs/fullchain.pem+privkey.pemand asks Postfix and Dovecot to reload; Caddy loads it through its rendered config. - Renewal: repeat step 1’s API call (
PUT /api/v1/setup/tlsis locked after setup; usePUT /api/v1/server/configwithtls.mode: byoafter replacing the files — an admin endpoint to upload a new certificate is planned; until then copy the files into themailserver_certsvolume underbyo/, e.g.docker run --rm -v mailserver_certs:/certs -v $PWD:/in alpine sh -c 'cp /in/fullchain.pem /in/privkey.pem /certs/byo/ && chown 2525:2525 /certs/byo/*.pem && chmod 640 /certs/byo/privkey.pem'— the key must stay group 2525 and 0640, orcert-synccannot read it). cert-sync picks up the change within 30 s, for Postfix and Dovecot only. Caddy reads the files once, when its configuration is loaded, and a configuration identical to the one it holds is not reloaded, so the HTTPS names keep serving the old certificate until it restarts: rundocker restart mailserver-caddy-1after copying the files.
Verification
Section titled “Verification”openssl s_client -connect <hostname>:993 -servername <hostname> </dev/null | openssl x509 -noout -subject -dates shows your certificate; same on :587 with -starttls smtp; https://<hostname>/, https://postmaster.<your domain>/ and https://webmail.<your domain>/ are all trusted by the browser — run the last two explicitly, because a certificate that covers only the mail hostname fails on exactly those and nowhere else.
Rollback
Section titled “Rollback”Switch to tls.mode: acme via PUT /api/v1/server/config and delete /certs/byo/* from the volume. cert-sync installs the mail hostname’s ACME certificate once Caddy has obtained it; until then Postfix and Dovecot keep serving the last BYO certificate it copied. It does not fall back to the self-signed bootstrap certificate, which it generates only at start and only when no certificate is installed at all.