insert

📮 Run yourself a mail server


Guide

Make a compose project:
(using acmesh for certs, unbound for DNS, and maddy for the mail server itself)

# compose.yml
name: maddy
services:
  # TLS certificates
  acmesh-daemon:
    image: neilpang/acme.sh
    volumes:
      - "/data/maddy_acme.sh:/acme.sh"
    command: daemon
    restart: always
    env_file: ./.env
    networks:
      maddy-network:
        aliases:
          - acmesh

  # dns server
  unbound:
    image: mvance/unbound:latest
    # for arm64:
    # image: mvance/unbound-rpi
    restart: always
    networks:
      maddy-network:
        ipv4_address: ${IPV4_NETWORK:-10.8.1}.254
        aliases:
          - unbound

  # email server
  maddy:
    # check https://github.com/foxcpp/maddy/releases
    image: ghcr.io/foxcpp/maddy:0.8.1
    volumes:
      - "/data/maddy_acme.sh:/etc/maddy/certs"
      - "/data/maddy:/data"
      - type: bind
        source: ./maddy.conf
        target: /etc/maddy/maddy.conf
    ports:
      - "25:25"
      - "143:143"
      - "465:465"
      - "587:587"
      - "993:993"
    entrypoint: /bin/maddy -config /etc/maddy/maddy.conf
    command: run
    restart: always
    networks:
      maddy-network:
        aliases:
          - maddy
    dns:
      - ${IPV4_NETWORK:-10.8.1}.254

networks:
  maddy-network:
    driver: bridge
    driver_opts:
      com.docker.network.bridge.name: br-maddy
    ipam:
      driver: default
      config:
        - subnet: ${IPV4_NETWORK:-10.8.1}.0/24
        - subnet: ${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}

Create configuration files:

We have to make some changes to the config file:

# maddy.conf
# (1) change the hostname

# change these to where you want your mail server to exist
$(hostname) = example.org
$(primary_domain) = example.org
$(local_domains) = $(primary_domain)
# ^^ you can add more domains to local_domains too

# for example:
$(hostname) = mx1.is.horse
$(primary_domain) = is.horse
$(local_domains) = $(primary_domain) insrt.uk
# this means my mail server will live at mx1.is.horse
# and send/receive mail for is.horse and insrt.uk

# (2) add reverse DNS hostname if you are sending from the machine you're running this on
# ... otherwise scroll down to configuration notes on how to do SMTP forwarding through smth else
smtp tcp://0.0.0.0:25 {
    ...

    dmarc yes
+   hostname fully-qualified-host.example.com
    check { }

    ...
}

target.remote outbound_delivery {
    limits { }
+   hostname fully-qualified-host.example.com
    mx_auth { }
}

Now let’s make the certificate:

docker compose up -d acmesh-daemon
docker compose exec acmesh-daemon --register-account -m me@insrt.uk
docker compose exec acmesh-daemon mkdir -p /etc/maddy/certs/mx1.is.horse
docker compose exec acmesh-daemon --issue --dns dns_cf -d mx1.is.horse --key-file /etc/maddy/certs/mx1.is.horse/privkey.pem --fullchain-file /etc/maddy/certs/mx1.is.horse/fullchain.pem

Now you need to configure DNS, pls RTFM maddy docs here, it has everything you need. NB. if you’re using mail forwarding (i.e. Scaleway TEM, see configuration notes) then you don’t need to copy the DKIM as you’re not sending any mail from your IP!

Final stretch, bring all services up and create your first mail account:

docker compose up -d

# create credentials
docker compose exec maddy /bin/maddy -config /etc/maddy/maddy.conf creds create postmaster@is.horse
# create imap storage
docker compose exec maddy /bin/maddy -config /etc/maddy/maddy.conf imap-acct create postmaster@is.horse

Make sure to port-forward and open ports!

sudo ufw allow 25 comment 'maddy SMTP'
sudo ufw allow 143 comment 'maddy IMAP'
sudo ufw allow 993 comment 'maddy IMAP'
sudo ufw allow 465 comment 'maddy submission'
sudo ufw allow 587 comment 'maddy submission'

Done! You can now login and hopefully send email.

Configuration Notes

How do I run this at home?

In all likelihood, the ports you need are blocked and your IP has garbage reputation, you have a couple of options:

  • Rent a VPS to proxy your mail server (e.g. fast reverse proxy) and use an external mail sender like TEM (see next section)
  • Rent a VPS to run this on (and likely still run into the reputation issue, see next section)
  • Don’t - out of scope of this article

How do I use a 3rd party SMTP sender so I don’t get blocked?

First, sign up with a provider such as Scaleway and add your domains to TEM (or SES, or whatever), they will give you a username and SMTP host, you have to create an API key which is used as the password, then:

# maddy.conf
# (1) add your delivery route
submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
    ...
    source $(local_domains) {
        ...
        default_destination {
-           modify {
-               dkim $(primary_domain) $(local_domains) default
-           }
-           deliver_to &remote_queue
+           deliver_to smtp tcp://smtp.tem.scaleway.com:587 {
+               auth plain USERNAME PASSWORD
+           }
        }
    }
}

# (2) remove the old queue blocks
- target.remote outbound_delivery {
-   ...
- }

- target.queue remote_queue {
-   ...
- }

I want to alias or wildcard my email?

Read https://maddy.email/reference/modifiers/envelope/#envelope-sender-recipient-rewriting.

e.g. you could:

msgpipeline local_routing {
    destination postmaster $(local_domains) {
        modify {
            replace_rcpt &local_rewrites
+           replace_rcpt regexp "(.+)@is.horse" "me@insrt.uk"
+           replace_rcpt regexp "(.+)@insrt.uk" "me@insrt.uk"
        }
    }
}

How do I make my email client pick up config automatically?

Add this to your configuration:

# compose.yml
services:
  mail-autodiscover-autoconfig:
    image: wdes/mail-autodiscover-autoconfig:latest
    restart: always
    ports:
      - "8088:80"
    environment:
      ROCKET_PROFILE: production
      ROCKET_ADDRESS: "0.0.0.0"
      ROCKET_PORT: "80"

      # set to where your mail server lives
      IMAP_HOSTNAME: mx1.is.horse
      SMTP_HOSTNAME: mx1.is.horse
      POP_HOSTNAME: mx1.is.horse

      # regenerate both UUIDs using https://uuidgenerator.net
      APPLE_MAIL_UUID: 3b88a342-66fd-4b7e-9f6a-d9199f7536e5
      APPLE_PROFILE_UUID: ae7ffc8c-dc1b-4fa3-abbe-78048dc58f42

Configure reverse proxy of port 8088 to all your relevant domains: autoconfig.your.domain

Webmail

Roundcube

Add the following configuration:

# compose.yml
services:
  webmail:
    image: roundcube/roundcubemail
    ports:
      - "8080:80"
    volumes:
      - "./config.inc.php:/var/www/html/config/config.inc.php"
      - "/data/maddy_roundcube_db:/var/roundcube/db"
    environment:
      ROUNDCUBEMAIL_DB_TYPE: sqlite
      ROUNDCUBEMAIL_DEFAULT_HOST: tls://mx.your.mail
      ROUNDCUBEMAIL_SMTP_SERVER: tls://mx.your.mail
      ROUNDCUBEMAIL_INSTALL_PLUGINS: 1
      ROUNDCUBEMAIL_PLUGINS: archive,zipdownload
    depends_on:
      - maddy
    restart: always
    networks:
      maddy-network:
      aliases:
        - webmail

Configure reverse proxy of port 8080 to some HTTPS endpoint like mail.your.domain.

# config.inc.php
<?php
    $config['log_driver'] = 'stdout';
    $config['support_url'] = 'https://your.website';
    $config['product_name'] = 'Cool Mail Server';
    $config['skin_logo'] = 'https://path.to/logo.svg';

    // change to random 24 character string!
    $config['des_key'] = 'AAAAAAAAAAAAAAAAAAAAAAAA';

    $config['plugins'] = [
        'archive',
        'zipdownload',
    ];

    $config['zipdownload_selection'] = true;
    $config['enable_spellcheck'] = true;
    $config['spellcheck_engine'] = 'pspell';
    $config['skin'] = 'elastic';

    include(__DIR__ . '/config.docker.inc.php');
Mutant Standard emoji 2020.04 Dzuk http://mutant.tech/ 🇪🇺 🇬🇧 🇵🇱
© 2025 · Built using Astro