Skip to content
UnbeholdenWhat I run, what I build

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.

Self-hosting · 20 September 2026 · 6 minutes

A row of colourful mailboxes, numbered and on wooden posts, tangled in dry vines with yellow flowers behind.

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.

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. 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. 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.

Mail ports skip the proxy, so Stalwart sees real IPs The VPS sits behind the provider firewall, whose four inbound rules let TCP 25, 465, 587 and 993 in from anywhere. Other mail servers deliver inbound mail on port 25, and mail clients submit and read mail on 465, 587 and 993. Those ports go straight from the host to the Stalwart container, so Stalwart sees the real IP of whoever connects, and SPF checks, rate limits and IP bans work. HTTP and HTTPS on 80 and 443 go through Traefik, run by Dokploy, for the admin UI and autodiscovery. Traefik forwards those requests, so Stalwart sees only Traefik's internal IP; a ban on that IP takes the web side down, and the fix is putting the Docker subnets on Stalwart's allowed IP list. Traefik gets web certificates with the HTTP challenge; Stalwart gets its own with DNS-01 through the registrar API. Stalwart relays outbound mail through SMTP2Go as an MTA route, which delivers it to recipients' servers and sees outbound metadata only. PROVIDER FIREWALL · VPS Mail servers Mail clients HTTP clients Traefik SMTP2Go Recipients Stalwart other domains phone, desktop admin, autodiscovery Dokploy container their mail servers TLS: HTTP challenge SPF, rate limits, IP bans work TLS: DNS-01, registrar API Sees outbound metadata only sees real IPs sees Traefik's IP 25 · delivers inbound mail 465 · 587 · 993 · submits, reads 80 · 443 forwards relays · MTA route delivers Ban Traefik's IP and the web side goes down Fix: Docker subnets on the allowed IP list

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 monthly fees.

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.