UnbeholdenDigital sovereignty in practice

Self-hosting / Email · 20 September 2026 · 6 minutes

Self-hosting my email with Stalwart

Everyone says don't self-host email. I did it anyway, and most of the warnings turned out to be out of date or fixable.

A worn brass letterbox set into a dark painted door, lit warm from one side, with an indigo light glowing from the gap behind the flap.

The standard advice is not to self-host email. Nothing you send will get delivered, the big providers will bin it, and you’ll lose your weekends to spam filters. I did it anyway. It took one long day, and most of those warnings turned out to be out of date or fixable. I now run mail for several domains and a few family members on a small VPS.

Here’s why I bothered, what it costs, and the three problems that ate most of that day.

Why bother

Most of the usual reasons aren’t very good. Small inboxes, no custom domains, ads in the web client: Fastmail or Migadu fix all of that for a few euros a month. If that were all, I’d have just paid.

What got me is that every account recovery ends up in your inbox. Password resets, “verify it’s you” links, all of it. If you rent that inbox, whoever you rent it from holds the keys to everything else, and they can take them back. Usually not on purpose: an automated system flags your account, the appeal form goes nowhere, and you’re locked out of your bank, your registrar and your tax account at the same time. The write-ups from people it has happened to are grim reading. It’s unlikely, but if it happens it takes everything with it.

Then there’s the big free providers scanning your mail as part of the business, and a more practical point: no provider, at any price, gives you what your own server does. Unlimited domains and addresses, a catch-all on each domain so you can make up a new address for every sign-up, plus calendar, contacts and file sync.

What it costs

An outbound relay, and the metadata that comes with it. I send through SMTP2Go rather than straight from the VPS. It’s the one real compromise here, so it’s worth explaining. Getting mail from port 25 delivered comes down to reputation, which takes years to build and favours the big players. A fresh VPS IP starts out somewhere between suspicious and blocked. SMTP2Go’s free tier covers personal volume, and the price is that one company sees my outbound metadata: who I write to and how often. Incoming mail, which is most of it, never goes near them. It’s also reversible. Sending directly is a config change, not a rebuild, if I ever want to spend months warming up an IP.

You’re the ops team. Backups, updates, and the chance something breaks while you’re up a mountain. That’s less scary than it sounds, because sending servers retry for days. If mine is down, mail arrives late, not never.

A long day. Mine was about fourteen hours, and most of it went on the three problems below. Knowing them in advance, I reckon it’d take two.

The setup

The pieces:

  • A VPS from a provider that allows mail traffic. Its firewall is the outer boundary.
  • Stalwart, a mail server written in Rust. SMTP, IMAP, JMAP, spam filtering, a web admin and CalDAV/CardDAV, in one binary. Having used it, I wouldn’t bother wiring Postfix, Dovecot and Rspamd together for a personal server.
  • Dokploy with Traefik, already on the box, owning ports 80 and 443 and handling TLS for the web side (admin UI, client autodiscovery).
  • SMTP2Go for outbound, set up as an MTA route in Stalwart.
  • An API token for my registrar, so Stalwart manages each mail domain’s DNS records itself, including DKIM keys and the TXT records for its own certificates.

The decision that mattered most was keeping the mail ports away from the proxy. Ports 25, 465, 587 and 993 go straight from the container to the host, and only HTTP goes through Traefik. SPF checks, rate limiting and IP bans all need the real IP of whoever’s connecting, and a proxy hides it. That heads off a whole class of problems and causes one, which I’ll get to.

The provider firewall has four inbound rules:

TCP 25   from anywhere   (SMTP, other servers delivering to you)
TCP 465  from anywhere   (submission, implicit TLS)
TCP 587  from anywhere   (submission, STARTTLS)
TCP 993  from anywhere   (IMAPS)

TLS is split in two, which confused me at first. Traefik gets certificates for the web hostnames the usual way. Stalwart gets its own for the mail ports with the DNS-01 challenge, writing the validation record through the registrar API. Both renew on their own.

The rest is uneventful. You create one account, add as many addresses on as many domains as you want, turn on a catch-all per domain, and everything from Thunderbird to a phone picks up the settings from the DNS records Stalwart published. Adding a second domain is one Domain object, a registrar API token if it’s registered elsewhere, and an SPF edit.

Three things that cost me hours

1. The proxy gets itself banned

Stalwart bans IPs automatically, and it’s good at it. Brute-force a login or scan ports and your IP gets dropped at the socket. The trouble is that every HTTP request through a reverse proxy arrives from the proxy’s internal IP. So the proxy takes the blame for the whole internet and eventually gets banned itself. When that happens, everything behind it goes down together: admin UI, autodiscovery, the lot. The docs warn about exactly this, in a paragraph I only read afterwards. In my case the ban even survived deleting the blocked IP entry, and the only fix that worked was moving the proxy to a different internal address.

IP-based security only works where the real IPs are visible. The mail ports see them, so bans there hit actual attackers. The HTTP side doesn’t, so exempt it: put your Docker subnets on Stalwart’s allowed IP list on day one.

2. Two ACME clients can’t share port 80

Traefik uses the HTTP challenge, so it owns /.well-known/acme-challenge/ on the whole entrypoint. When Stalwart tried to get a certificate the same way, Traefik caught the validation requests and returned 404 for tokens it didn’t know. Its log says so plainly: Cannot retrieve the ACME challenge for mail.unbeholden.dev. So Stalwart has to use DNS-01, which doesn’t need any ports.

It goes the other way too. Stalwart publishes CAA records tied to its own ACME account, which quietly stops Traefik issuing anything for that domain. I had to loosen the record and tell Stalwart to stop republishing it.

3. Declare the network in compose

Dokploy attaches Traefik to your app’s Docker network on the fly when you add a domain. That works until Traefik restarts and the connection drops. Then you get Bad Gateway, a perfectly healthy container, and nothing in any log. The fix is one block in the compose file, straight from Dokploy’s docs, attaching the service to the shared dokploy-network explicitly. Any Dokploy compose app with a domain needs it.

Where it ended up

It scored 10/10 on mail-tester the first time I ran it. SPF, DKIM and DMARC all pass and align. I’ve got as many addresses on as many domains as I like, a catch-all so each service gets its own address, server-side filtering, and calendar, contacts and file sync over CalDAV, CardDAV and WebDAV, which my clients picked up during autodiscovery without me asking. Running cost: a VPS I already had, the domains, and no subscriptions.

More to the point, the inbox every other account falls back to now runs on a server I control, from config I can read, and there’s no account manager anywhere who can switch it off.

Should you?

If you’ve never run a server, no. Start with something that doesn’t hurt when it breaks.

If you already run your own infrastructure and keep it patched, and the account-recovery argument lands, self-hosted email in 2026 is far more manageable than its reputation. Stalwart turns five daemons into one, the relay handles deliverability for the price of some metadata, and what’s left is the three problems above, which you now know about.