About a year ago I wanted to expand beyond namecheap's simple "email forwarding" service and start setting up my own mailboxes. At the time, I was looking for a solution that I could host on my docker swarm (obviously). And the defacto suggestion for a self-hosted solution was docker-mailserver.
That solution worked for a while, but was relatively resource hungry. It's based a "recommended email solution in a box" with all the bells and whistles.
It's also worth noting that if you're going down the road of setting up your own mailserver, that mxtoolbox is invaluable for testing your server and DNS configuration. I don't think I could've gotten this far without them.
The Road to Maddy
This month I did another search to see if anything else came up recently, and discovered maddy. While their solution targets smaller deployments, it was significantly easier to setup. Their configuration is in a single volume, and setup involves basically spinning up, creating TLS certs, and setting up DKIM (And other records) in your DNS. On top of that, it seems to cap out around 15 MB of memory!
Given that I already had experience setting up a mailserver before, this took me only a few hours to get fully working.
I use rainloop as my webmail deployment.
Docker Compose
I use my mail setup on my same swarm that has traefik setup on it, so I prefer to reuse
the certificates traefik is grabbing for me. I do this by using my jq
snippet described here.
version: "3.5"
services:
maddy:
image: foxcpp/maddy:latest
ports:
- "25:25"
- "143:143"
- "587:587"
- "993:993"
volumes:
- maddydata:/data
environment:
# REPLACE DOMAINS WITH YOURS
- MADDY_HOSTNAME=mx.example.com
- MADDY_DOMAIN=example.com
deploy:
replicas: 1
resources:
limits: { cpus: '0.2', memory: '32M' }
reservations: { cpus: '0.05', memory: '16M' }
# Hit /?admin for setup
# Webmail
rainloop:
image: hardware/rainloop
deploy:
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-net"
- "traefik.http.routers.rainloop.rule=Host(`mail.example.com`)"
- "traefik.http.services.rainloop.loadbalancer.server.port=8888"
- "traefik.http.routers.rainloop.entrypoints=websecure"
resources:
limits: { cpus: '0.2', memory: '64M' }
reservations: { cpus: '0.05', memory: '32M' }
volumes:
- rainloop-data:/rainloop/data
networks:
- traefik-net
- default
# Synchronize certificates from traefik/acme.json to maddy every 24 hours
certsync:
image: stedolan/jq
entrypoint: |
/bin/bash -c "
jq -r '.le.Certificates[] | select(.domain.main==\"'mx.example.com'\") | .certificate' /data/acme.json | base64 -d > /out/tls_cert.pem;
jq -r '.le.Certificates[] | select(.domain.main==\"'mx.example.com'\") | .key' /data/acme.json | base64 -d > /out/tls_key.pem;
"
volumes:
- common_letsencrypt:/data:ro
- maddydata:/out
deploy:
mode: global
placement:
constraints: [node.role==manager]
restart_policy:
delay: 24h
resources:
limits: { cpus: '0.1', memory: '32M' }
reservations: { cpus: '0.025', memory: '16M' }
networks:
# The network that can talk to traefik
traefik-net:
external: true
name: traefik-net
volumes:
maddydata: {}
rainloop-data: {}
common_letsencrypt: # Wherever you store your acme.json file from traefik
external: true