Skip to content

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.

  1. At setup: tls.mode: byo with certificatePem / privateKeyPem in the answers file (or the wizard’s TLS step). Multi-line YAML:
    tls:
    mode: byo
    certificatePem: |
    -----BEGIN CERTIFICATE-----
    …
    privateKeyPem: |
    -----BEGIN PRIVATE KEY-----
    …
  2. The API writes them to the certs volume (/certs/byo/fullchain.pem, privkey.pem 0640 group 2525, which cert-sync reads through that group). cert-sync prefers a BYO certificate over ACME, copies it to /certs/fullchain.pem + privkey.pem and asks Postfix and Dovecot to reload; Caddy loads it through its rendered config.
  3. Renewal: repeat step 1’s API call (PUT /api/v1/setup/tls is locked after setup; use PUT /api/v1/server/config with tls.mode: byo after replacing the files — an admin endpoint to upload a new certificate is planned; until then copy the files into the mailserver_certs volume under byo/, 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, or cert-sync cannot 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: run docker restart mailserver-caddy-1 after copying the files.

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.

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.