Running Mailu Behind Caddy with Let’s Encrypt (A Practical Guide)


Self‑hosting email is one of those projects that sounds simple until you actually try it. Between TLS, reverse proxies, Autodiscover, and modern mail client expectations, it’s very easy to end up with redirect loops, broken SMTP, or certificates that load everywhere except where you need them.

This post walks through a working, real‑world setup: running Mailu behind Caddy with Let’s Encrypt certificates (DNS‑01). It’s written from the perspective of what actually works, not just what looks correct on paper.

All domains, paths, and identifiers in this post are examples.




Why This Architecture

Mail servers are special:

They need TLS for web, IMAP, and SMTP

They need predictable hostnames

They must support Autodiscover for Outlook, Gmail, and mobile clients


The architecture below solves all of that cleanly:

Internet
   │
   ▼
[Caddy :443]  ← public HTTPS
   │
   ▼
[Mailu Front :443]  ← internal TLS
   │
   ├─ IMAP (993)
   ├─ SMTP (465)
   └─ Webmail / Admin

Key design choice:

> Caddy proxies to Mailu’s HTTPS port (443), not HTTP.



That single decision avoids most redirect and TLS issues.




Directory Layout (Example)

Here’s a clean, anonymized layout that works well:

/opt/mail/
├── mailu/
│   ├── data/
│   ├── mail/
│   ├── dkim/
│   ├── certs/
│   │   ├── cert.pem
│   │   └── key.pem
│   ├── docker-compose.yml
│   └── mailu.env

├── caddy/
│   ├── Caddyfile
│   └── docker-compose.yml

└── letsencrypt/

You can place this anywhere — /opt, /srv, or a mounted volume.




Certificates: Let’s Encrypt with DNS‑01

Mail servers benefit greatly from DNS‑01 validation:

No open ports required

Works with multiple subdomains

Ideal behind proxies


Requesting a Certificate (Example)

certbot certonly \
  –dns-provider \
  -d mail.example.com \
  -d autodiscover.example.com \
  -d autoconfig.example.com

After success:

/etc/letsencrypt/live/mail.example.com/
├── fullchain.pem
└── privkey.pem




Making Certificates Available to Mailu

Mailu expects certificates inside its own volume:

cp /etc/letsencrypt/live/mail.example.com/fullchain.pem \
   /opt/mail/mailu/certs/cert.pem

cp /etc/letsencrypt/live/mail.example.com/privkey.pem \
   /opt/mail/mailu/certs/key.pem

chmod 644 /opt/mail/mailu/certs/cert.pem
chmod 600 /opt/mail/mailu/certs/key.pem

If these files are empty or truncated, IMAP and SMTP will fail immediately.




Mailu Configuration (Minimal, Correct)

In mailu.env:

DOMAIN=example.com
HOSTNAMES=mail.example.com,autodiscover.example.com,autoconfig.example.com
TLS_FLAVOR=cert
ENABLE_AUTODISCOVER=true

WEB_ADMIN=/admin
WEB_WEBMAIL=/webmail

This ensures Mailu advertises the correct endpoints to clients.




Caddy: The Reverse Proxy That Makes This Work

Here’s the critical part.

Caddyfile

mail.example.com,
autodiscover.example.com,
autoconfig.example.com {

    @root path /
    redir @root /webmail 302

    reverse_proxy https://mailu-front:443 {
        transport http {
            tls_insecure_skip_verify
        }

        header_up Host {host}
        header_up X-Forwarded-Host {host}
        header_up X-Forwarded-Proto https
        header_up X-Forwarded-Port 443
    }
}

Why HTTPS → HTTPS Matters

Mailu internally enforces HTTPS. If you proxy to port 80, nginx will continuously attempt to “upgrade” requests — even when the client is already using HTTPS.

Result:

Infinite 301 redirects

Broken admin UI

Webmail never loads


Proxying to 443 avoids this entirely.




Starting the Stack

cd /opt/mail/mailu
docker compose up -d

cd /opt/mail/caddy
docker compose up -d

Confirm everything is running:

docker ps




Verifying the Web Interfaces

curl -Ik https://mail.example.com/admin/
curl -Ik https://mail.example.com/webmail/

Expected:

No redirect loops

Login page loads





Verifying Mail Protocols

IMAP (993)

openssl s_client -connect mail.example.com:993 \
  -servername mail.example.com -brief </dev/null

SMTP (465)

openssl s_client -connect mail.example.com:465 \
  -servername mail.example.com -brief </dev/null

Modern Mailu deployments often do not support STARTTLS on 587. Port 465 is the correct choice.




Autodiscover Test

curl -X POST https://autodiscover.example.com/autodiscover/autodiscover.xml \
  -H “Content-Type: text/xml” \
  –data ‘<Autodiscover xmlns=”http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006″>
  <Request>
    <EMailAddress>user@example.com</EMailAddress>
    <AcceptableResponseSchema>
      http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a
    </AcceptableResponseSchema>
  </Request>
</Autodiscover>’

Clients should receive IMAP and SMTP settings automatically.




Automatic Certificate Renewal

certbot renew –deploy-hook “
  docker restart mailu-front \
  docker restart mailu-imap \
  docker restart mailu-smtp


This ensures mail protocols reload certificates correctly.




Lessons Learned

Proxying Mailu via HTTP causes redirect loops

Empty certificate files break IMAP/SMTP instantly

Port 465 is now the safest SMTP option

DNS‑01 validation is ideal for mail servers





Final Thoughts

Mail servers don’t fail loudly — they fail subtly. A single redirect, missing TLS flag, or incorrect port can break everything downstream.

This setup has proven reliable, debuggable, and future‑proof. If you’re self‑hosting mail today, this architecture will save you hours of frustration.




All paths and domains shown are examples. Replace them with values appropriate to your environment.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Secret Link