Hosting a Mail Server

This guide requires a system with Podman and Podman Compose installed. Read our guide on Podman

Introduction

Nowadays, federated protocols such as the “fediverse” (ActivityPub) and Matrix are the hottest new things.

But federation is not a new concept. In fact, one of the first internet systems ever, email (est. 1971), is essentially a federated system, with many independently operated servers communicating with one another, each server having its small subset of users.

In the early days, this was exactly how things worked. But as the Internet grew, spammers, email spoofing and phishing became more and more of a threat. People came up with IP blacklists, DKIM, SPF, DMARC and about 9000 other things you need to keep in mind, making it increasingly harder to host your own mail server.

As always, companies that profit off this difficulty appeared, selling you access to their preconfigured mail servers for a small fee.

And, as the “FREE STUFF!!! your data is mine, you are the product, watch what you say” business model that we all know and love today became mainstream, companies transitioned to giving you that service for FREE!, while also harvesting your supposedly private data and censoring you. Most people say it’s worth it for the convenience. But you’re not most people, right?

“But self-hosting email is impossible!”

A very good article on this topic: “[…]The oligopoly has won.

Obviously, these companies (referred to as “The Cartel” from now on) won’t just let you believe you can host your own server without paying for protection (with money or your data), so they use one of the oldest and easiest manipulation methods known to man: propaganda.

Unfortunately for The Cartel, many private and public entities, including governmental ones, self-host their email (as they should), because they don’t want to trust The Cartel with their highly sensitive conversations (they shouldn’t).

So yes, as long as you follow this guide very carefully and use a reputable host that doesn’t allow skids, phishers and/or spammers on their network and goes through the tedium of dealing with super annoying blacklist operators, it’s still very much possible.

If you self-host mail, you are taking one small step for your privacy, but one giant step for reclaiming the open and decentralized Internet.

Maybe one day we won’t even have to worry about deliverability to The Cartel. Get your friends on self-hosted email. Get a cool domain name and give them accounts on your server. You wouldn’t think they do, but normies actually like having a cool email domain. Bitches love my swag domain. Yeah,.

Prerequisites

You will need:

If your IP is blacklisted, read Blacklist Removal.

Initial DNS records

For now, you need 2 records:

| Type |        Name        |      Content      |
|------|--------------------|-------------------|
| A    | mail               | [server ip]       |
| MX   | example.com (or @) | mail.example.com  |

mail.example.com points to the mail server’s IP, and mail for example.com (e.g. user@example.com) is handled by mail.example.com.

If you use Cloudflare for DNS, make sure the records are unproxied (grey cloud).

Keep the tab open as we’ll be adding some more DNS records later on.

Rootless Port Limitations

Running a mail server requires binding ports below 1024, which is the lowest port a non-root user can normally bind. If you’re setting this up on a non-root account (which is a good idea), you have to use a workaround, such as adding the line below to /etc/sysctl.conf:

net.ipv4.ip_unprivileged_port_start=25

Then, apply the change:

$ sysctl -p

This allows every non-root user to bind any port greater than or equal to 25 (SMTP).

The Stack

We’ll be using docker-mailserver for this, as I have tried multiple solutions for self-hosting email but DMS was the easiest and least complicated one by far. It also contains everything inside a single container so you can use the same machine for other purposes, securely and without worrying about breaking something.

This guide is partly based on the full DMS documentation, refer to it for further options and features (including anti-spam and anti-malware).

We’ll set up the server without any kind of anti-spam or anti-malware. Use your brain, don’t rely on machines.

We’ll also be using Caddy for our SSL certificates, but we’ll get to that later.

Create a Compose file inside a new mail directory:

services:
  dms:
    restart: always
    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    hostname: mail.example.com
    # https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/
    ports:
      - "25:25"    # SMTP  (explicit TLS => STARTTLS)
      - "143:143"  # IMAP4 (explicit TLS => STARTTLS)
      - "465:465"  # ESMTP (implicit TLS)
      - "587:587"  # ESMTP (explicit TLS => STARTTLS)
      - "993:993"  # IMAP4 (implicit TLS)
    dns:
      - 9.9.9.9
      - 8.8.8.8
      - 1.1.1.1
      - 1.0.0.1
    environment:
      - SSL_TYPE=manual
      - SSL_CERT_PATH=/certs/mail.example.com.crt
      - SSL_KEY_PATH=/certs/mail.example.com.key
      - DOVECOT_INET_PROTOCOLS=ipv4
    volumes:
      - ./data/data:/var/mail
      - ./data/state:/var/mail-state
      - ./data/logs:/var/log/mail
      - ./config/:/tmp/docker-mailserver/
      - /etc/localtime:/etc/localtime:ro
      - ../caddy/data/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/:/certs/:ro
    healthcheck:
      test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1"
      timeout: 3s
      retries: 1

SSL

Before running DMS, you need a SSL certificate to encrypt your mail while it’s being transported over the Internet (mail is still stored in plaintext on disk unless you explicitly use an E2EE solution such as PGP/Enigmail).

I recommend running Caddy, a web server that automatically grabs Let’s Encrypt certificates and renews them before expiration.

I also recommend keeping Caddy as its own stack, because you can only run one Caddy instance per IP (since it binds ports 80 and 443). Keeping it in a separate stack allows you to serve multiple clearnet services from the same IP (by using external Podman networks to connect stacks together).

If you already have Caddy set up, skip to the Caddyfile part. If not, here’s a sample Compose file:

services:
  caddy:
    image: docker.io/library/caddy:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data:/data/caddy # caddy internal data

And, the Caddyfile entry:

mail.example.com {
# if you want to redirect users visiting
# mail.example.com via a web browser to
# somewhere else, uncomment below
#
#  redir https://example.com
#
# you can leave this commented and caddy
# will still get the certificates, serving
# a blank page
}

Bring Caddy up to generate the certificates:

$ podman-compose up -d

Wait a bit (podman-compose logs --tail 500 -f to check the logs) and check if the certificates have been successfully generated:

$ ls data/certificates/acme-v02.api.letsencrypt.org-directory/mail.example.com/

You should have 2 files in that directory, mail.example.com.crt and mail.example.com.key.

Setting Up Your Account

cd back to the mail directory and bring the stack up:

$ podman-compose up -d

Note: Do not use podman-compose stop or start with DMS, instead always use down and up -d

Create your account:

$ podman-compose exec dms setup email add user@example.com

Set up the postmaster alias:

$ podman-compose exec dms setup alias add postmaster@example.com user@example.com

You should be able to add your newly created account to your email client now. If you prefer using a browser for email, read Roundcube for instructions on setting up a web UI. Before sending your first email, there’s a few more things to do:

DKIM, DMARC & SPF

DKIM, DMARC and SPF are standard email security measures required to send and receive mail from pretty much anywhere. Refer to the DMS docs for further details.

Generate your DKIM key:

$ podman-compose exec dms setup config dkim

The DKIM DNS record should be located at mail/config/opendkim/mail.txt. It looks something like this:

mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ"
"5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB"
) ;

This is great if you’re running your own nameserver, but most people don’t, so in order to use this record in your DNS management interface of choice, you’ll have to extract the record content from that file.

Copy the content from between the parantheses, remove the double quotes, newlines and spaces between the quotes. You should be left with a single-line string that looks something like this:

v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQMMqhb1S52Rg7VFS3EC6JQIMxNDdiBmOKZvY5fiVtD3Z+yd9ZV+V8e4IARVoMXWcJWSR6xkloitzfrRtJRwOYvmrcgugOalkmM0V4Gy/2aXeamuiBuUc4esDQEI3egmtAsHcVY1XCoYfs+9VqoHEq3vdr3UQ8zP/l+FP5UfcaJFCK/ZllqcO2P1GjIDVSHLdPpRHbMP/tU1a9mNZ5QMZBJ/JuJK/s+2bp8gpxKn8rh1akSQjlynlV9NI+7J3CC7CUf3bGvoXIrb37C/lpJehS39KNtcGdaRufKauSfqx/7SxA0zyZC+r13f7ASbMaQFzm+/RRusTqozY/p/MsWx8QIDAQAB

Save this for later. Now, on to DMARC and SPF. Here are some non-restrictive starter records that ensure you receive all mail:

DMARC:

v=DMARC1; p=none; sp=none; fo=0; adkim=r; aspf=r; pct=100; rf=afrf; ri=86400; rua=mailto:dmarc.report@example.com; ruf=mailto:dmarc.report@example.com

SPF:

v=spf1 mx ~all

Add the new DNS records:

| Type |        Name        |             Content             |
|------|--------------------|---------------------------------|
| TXT  | mail._domainkey    | [extracted DKIM record content] |
| TXT  | _dmarc.example.com | [DMARC record content]          |
| TXT  | example.com (or @) | [SPF record content]            |

And, finally, restart DMS:

$ podman-compose down
$ podman-compose up -d

rDNS

Some strict mail servers reject emails from IPs that don’t have a correct PTR/rDNS record. A regular A DNS record points a domain to an IP address, but a PTR record does the opposite, pointing an IP address to a domain, hence reverse DNS. Kyun provides rDNS on demand, manageable through your Danbo dashboard (click the cog icon next to the IP).

Testing Delivery

Start sending test emails to The Cartel. If you misconfigured something, you should get an email back informing you of the issue. It’s best to have your own Cartel addresses for testing, as sometimes your emails may get silently dropped without receiving reports.

Blacklist Removal

Visit the blacklist site, enter your server’s IP and check for any removal sections. Most reputable blacklists usually remove the listing automatically on request after completing a CAPTCHA, provided the IP has not been recently abusive. Using a residential IP (not VPN/Tor) while doing the request might also help.

Note: Do not, under any circumstance, pay anyone for any removal.

Being on one or two no-name blacklists is usually not a problem, it’s still worth it to set up and test if you can send and receive mail from The Cartel.

If you’re a Kyun customer and need help or your request is refused, contact us and we’ll either help you with the requests or change your IP (of course, if you’re not the one that got the IP blacklisted).

Roundcube

You can skip this if you only plan on using native mail clients, but a web UI can be more convenient.

Create a new roundcube external network:

$ podman network create roundcube

And a new directory for our stack, containing the compose file:

services:
  roundcube:
    image: docker.io/roundcube/roundcubemail:latest
    restart: always
    volumes:
      - ./www:/var/www/html
      - ./db:/var/roundcube/db
    environment:
      - ROUNDCUBEMAIL_DB_TYPE=sqlite
      - ROUNDCUBEMAIL_DEFAULT_HOST=tls://mail.example.com
      - ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.example.com
    networks:
      - roundcube

networks:
  roundcube:
    external: true

Bring the stack up.

Connect your Caddy stack to the roundcube network:

services:
  caddy:
    image: docker.io/library/caddy:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data:/data/caddy # caddy internal data
    networks:
      - roundcube

networks:
  roundcube:
    external: true

Update your Caddyfile:

mail.example.com {
  reverse_proxy roundcube:80
}

Run podman-compose up -d to recreate the Caddy container. You should be able to access Roundcube via a browser at mail.example.com now.