Article Image
read

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
Blog Logo

Christopher LaPointe


Published

Image

Chris LaPointe

Another site of Code

Back to Overview