Goal
https://my.richardapplegate.io/webmailand/adminwork- Caddy serves the site on public port 443
- Mailu also serves HTTPS internally on front:443
- SMTP/IMAP ports (465/993/995/587) use the same Sectigo wildcard cert
- No port conflicts and no redirect loops
1) Certificates on the host
You have these two files (already confirmed they match):
fullchain.pemprivkey.pem
Store them in a stable host path, example:
/mnt/volumes/certs/fullchain.pem/mnt/volumes/certs/privkey.pem
✅ Verified match test:
openssl x509 -noout -modulus -in fullchain.pem | openssl md5
openssl pkey -noout -modulus -in privkey.pem | openssl md5
2) Caddy docker-compose
Caddy publishes public ports and has the certs mounted:
services:
caddy:
image: caddy:2
restart: unless-stopped
networks:
- caddy
ports:
- "80:80"
- "443:443/tcp"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data
- ./config:/config
- /mnt/volumes/certs:/certs:ro
networks:
caddy:
external: true
3) Mailu env (mailu.env) — corrected values
These are the critical “must be right” ones:
DOMAIN=richardapplegate.io
HOSTNAMES=my,mail
PORTS=25,465,587,993,995,4190
TLS_FLAVOR=cert
TLS_CERT_FILENAME=fullchain.pem
TLS_KEYPAIR_FILENAME=privkey.pem
WEB_ADMIN=/admin
WEB_WEBMAIL=/webmail
WEBSITE=https://my.richardapplegate.io
✅ Important notes:
DOMAINis the apex domain:richardapplegate.ioHOSTNAMESare labels only:my,mail(no dots)TLS_FLAVORiscert(notcerts)- Do not include
80or443inPORTS(Caddy owns those publicly)
4) Mailu docker-compose networking + cert mounts
4.1 Attach front to BOTH networks
Mailu internal network (for smtp/imap/etc) and the caddy network (so Caddy can reach it):
services:
front:
networks:
- mailu
- caddy
4.2 Mount certs into Mailu containers that use them
At minimum, mount into front, smtp, and imap (names may vary, but these are typical):
services:
front:
volumes:
- /mnt/volumes/certs/fullchain.pem:/certs/fullchain.pem:ro
- /mnt/volumes/certs/privkey.pem:/certs/privkey.pem:ro
smtp:
volumes:
- /mnt/volumes/certs/fullchain.pem:/certs/fullchain.pem:ro
- /mnt/volumes/certs/privkey.pem:/certs/privkey.pem:ro
imap:
volumes:
- /mnt/volumes/certs/fullchain.pem:/certs/fullchain.pem:ro
- /mnt/volumes/certs/privkey.pem:/certs/privkey.pem:ro
(These filenames match your TLS_CERT_FILENAME / TLS_KEYPAIR_FILENAME.)
5) Caddyfile (the working site)
You terminate TLS at Caddy and proxy to Mailu front over internal HTTPS to avoid redirect loops:
my.richardapplegate.io {
tls /certs/fullchain.pem /certs/privkey.pem
# optional: make / go to webmail
@root path /
redir @root /webmail 302
reverse_proxy https://front:443 {
transport http {
tls_server_name my.richardapplegate.io
# Only if chain issues ever happen:
# tls_insecure_skip_verify
}
header_up Host {host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
header_up X-Forwarded-For {remote_host}
}
}
✅ Why HTTPS upstream (front:443)?
Because with TLS_FLAVOR=cert, Mailu front enforces HTTPS; proxying to http://front:80 triggers redirect loops. HTTPS upstream avoids that entirely.
6) Restart order (clean rebuild)
6.1 Restart Mailu (force recreate so config regenerates)
cd /mnt/volumes/SamsungSSD970EVOPlus2TB/mailu
docker compose down
docker compose up -d --force-recreate
6.2 Reload Caddy
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
7) Verification tests
7.1 Web cert (public 443 via Caddy)
openssl s_client -connect my.richardapplegate.io:443 -servername my.richardapplegate.io </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
7.2 Mailu front cert (internal, from inside caddy container)
docker exec caddy sh -lc \
'openssl s_client -connect front:443 -servername my.richardapplegate.io </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates'
7.3 SMTP TLS cert (465)
openssl s_client -connect my.richardapplegate.io:465 -servername my.richardapplegate.io </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
7.4 IMAPS cert (993)
openssl s_client -connect my.richardapplegate.io:993 -servername my.richardapplegate.io </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Expected: all show CN=*.richardapplegate.io and Sectigo issuer.
Leave a Reply