Timothy SkidmoreNotes from building things at home

Self-hosted email · guide

Run your own family mail server, start to finish

How to host mailboxes for a family, and optionally a few AI assistants, on a small computer at home: real certificates, mail that reaches inboxes, and nothing exposed but the one port email needs. Replace example.com and the sample addresses with your own.

  • 25 incoming mail · the only port on the internet
  • 993 reading mail · home / VPN only
  • 465 sending from apps · home / VPN only
  • 587 outgoing relay

1What you end up with

  • Mailboxes like [email protected] and [email protected].
  • Phones that read and send mail at home on Wi-Fi, and away from home through a VPN back to the house.
  • Mail that arrives directly from the internet, and leaves through a sending service so it lands in inboxes instead of junk folders.
  • A real TLS certificate, signed mail (DKIM), DNSSEC, strict encryption policies and encrypted nightly backups.
  • Optionally, mailboxes for AI assistants that the server restricts to an allowlist of senders and recipients.

2The pieces

PieceOne good choiceWhy
Mail serverStalwart (open source) in DockerA single program with a web admin, spam filter, DKIM/SPF/DMARC and mail rules built in; very light on memory
DNSCloudflare (free)An API for automatic certificates and DKIM, plus one-click DNSSEC
CertificatesLet's Encrypt using the DNS-01 challengeWorks without exposing a web port
Outgoing mailA sending service with a free tier (e.g. SMTP2GO)Home IP addresses are on blocklists that major providers honour
Remote accessYour router's VPNKeeps login ports off the internet
BackupsA scheduled script to a NASEncrypted, verified, off the mail machine

Running cost: a domain name plus electricity.

3Decide where the server lives

A mail server needs incoming TCP port 25. Check your internet plan first: many cable, DSL and satellite plans block port 25 or share one public address among customers (CGNAT); many fiber plans don't.

  • Test outgoing in PowerShell: Test-NetConnection gmail-smtp-in.l.google.com -Port 25, or on Linux/Mac: nc -vz gmail-smtp-in.l.google.com 25.
  • Incoming has to be tested from outside your network, for example with an online SMTP port checker.

Port 25 works at home

Run the server at home and forward port 25 on your router to it. Simplest.

Port 25 is blocked

Keep the server at home and use a small cloud server, or another location you control, as a front door for port 25 only, connected by a WireGuard tunnel (section 8).

Hardware: 1–2 CPU cores and 1–2 GB of RAM is plenty for a family. An always-on Linux box or a VM/container on a home server works best. A Windows PC with Docker Desktop also works, with a few quirks covered in section 14.

4DNS and domain setup

  1. Move the domain's DNS to Cloudflare and turn on DNSSEC.
  2. If any other mail forwarding is enabled for the domain, turn it off before going live; it owns the MX records.
  3. Create an API token limited to editing DNS on this one domain, and store it securely.
  4. Add these records as you reach each step:
TypeNameValueNotes
Amailyour public IP (or the front door's)DNS only: mail can't go through Cloudflare's proxy
MX@mail.example.com, priority 10Add only when ready to receive
TXT@v=spf1 include:<your sending service> ~allSPF
TXT_dmarcv=DMARC1; p=none; adkim=r; aspf=rStart with p=none, tighten later
TXT<selector>._domainkeypublished by the mail serverDKIM
CNAMEas given by the sending serviceits bounce and DKIM hostsDNS only

If your IP address changes, keep the mail record updated with a small dynamic-DNS script.

5Install the mail server

A minimal Docker Compose file. Pin the image to the current minor release, and read the release notes before major upgrades.

services:
  stalwart:
    image: stalwartlabs/stalwart:<current release>
    restart: unless-stopped
    environment:
      STALWART_PUBLIC_URL: https://mail.example.com:<admin port>
    ports:
      - 127.0.0.1:8080:8080        # first-time setup, this machine only
      - 993:993                    # reading mail   - home/VPN only
      - 465:465                    # sending mail   - home/VPN only
      - <admin port>:443           # web admin      - home/VPN only
      - 25:25                      # incoming mail  - the only port on the internet
    volumes:
      - stalwart-etc:/etc/stalwart
      - stalwart-data:/var/lib/stalwart
volumes:
  stalwart-etc:
  stalwart-data:
  1. Get the temporary admin password from the container log (search for "bootstrap mode"). Each restart before setup prints a new one.
  2. Run the setup wizard at http://localhost:8080/admin: your hostname and domain, DKIM keys on, automatic TLS off for now, the default storage and directory, and logging to the console.
  3. Copy the permanent admin login from the final screen with the copy button and save it in a password manager.
  4. Sign in, change the admin password, and check that logging is actually enabled.

6Certificates via DNS

Connect the mail server to Cloudflare with your token, let it publish only the records you choose (start with DKIM and TLS reporting so it can't overwrite your MX early), turn on automatic DKIM, and set the certificate challenge to DNS-01. It then obtains a wildcard certificate and renews it on its own.

7Mailboxes and phones

  • Create a mailbox per person with a temporary password made of unambiguous characters, and have each person change it.
  • On your router, give the server a fixed IP and add a local DNS record for mail.example.com pointing to it.
  • In the phone's mail app: IMAP host mail.example.com port 993 SSL, SMTP host mail.example.com port 465 SSL, username = the full email address.
  • If someone enables two-factor sign-in, their mail app needs an app password. Create it on the phone itself and paste it.
  • Away from home, connect the router's VPN, ideally set to connect automatically.
  • On iPhone, if mail works away from home but not on home Wi-Fi after the domain goes public, turn off "Limit IP Address Tracking" for the home network.

8A remote front door for port 25

If your own connection can't accept port 25, a cloud server or another site you control can receive it and pass it to your home server through WireGuard.

  • On the front door: create a WireGuard peer for the mail server, forward TCP 25 to it, and ideally restrict that peer to port 25 only. If you use a firewall rule for that, make it match new connections only, or replies to incoming mail get blocked.
  • Avoid source NAT/masquerading on that forward if you can. Otherwise every sender appears to come from the front door, which weakens SPF and spam checks.
  • At home, run the mail server inside a WireGuard container's network and use split routing: only port 25 traffic uses the tunnel; everything else uses your own connection.
  • An alternative that always keeps real sender addresses: a small TCP proxy on the front door that speaks the PROXY protocol, which the mail server can read.

9Sending that reaches inboxes

550 5.7.1 · listed in Spamhaus PBLResidential IP addresses, including fiber, are usually on the Spamhaus PBL. Large providers reject or junk mail sent directly from them. Receiving isn't affected.
  1. Sign up for a sending service's free tier and verify your domain manually (adding only the records it lists, rather than granting it access to your DNS account).
  2. Restrict the account to sending from your domain only.
  3. Create an SMTP login for the mail server and store it securely.
  4. In the mail server, add a relay route to the service on port 587 and send all non-local mail through it.
  5. Publish SPF, send a test to an outside mailbox you own, and confirm open/click tracking is off.

10Encryption guarantees: MTA-STS and DANE

DANE (requires DNSSEC)

  1. Set certificate renewals to reuse the same key, so the record never goes stale.
  2. Hash the certificate's public key (SPKI, SHA-256) with a binary-safe tool such as openssl in bash.
  3. Publish a TLSA record: name _25._tcp.mail, usage 3, selector 1, matching type 1, with that hash.

MTA-STS

  1. Serve https://mta-sts.example.com/.well-known/mta-sts.txt. A free Cloudflare Worker with a custom domain is the simplest host and doesn't depend on your home server.
  2. Publish TXT _mta-sts = v=STSv1; id=<date>.
  3. Start in mode: testing; after a couple of quiet weeks switch to mode: enforce with a longer max_age, and change the id.
version: STSv1
mode: testing
mx: mail.example.com
max_age: 86400

Make sure the address in your TLS reporting record (usually postmaster@) exists as a mailbox or alias.

11Backups

Each night, stop the mail server for a few seconds, archive its data volumes, restart it, encrypt the archive (it contains every message, the DKIM private keys and your service credentials), copy it to a NAS, then decrypt the copy to prove it's good. Write the passphrase on paper and test a full restore once.

12Hardening checklist

  • Only port 25 open to the internet; admin, reading and sending ports reachable from home and VPN only.
  • A host firewall rule limiting the mail ports to your home and VPN address ranges.
  • Admin password in a password manager, with two-factor on the admin account.
  • DNSSEC on; DMARC moved from p=none to p=quarantine once mail flows normally.
  • MTA-STS enforced and DANE published.
  • Sending service limited to your domain, with tracking off.
  • Spam blocklist lookups working (see section 14).
  • Encrypted, tested backups.
  • Regular updates for the mail server, tunnel software and host.
  • Rotate any password or key that ever appears in a screenshot or chat.

13If AI assistants use mailboxes

  • Give each assistant its own mailbox and password.
  • Run each assistant under its own operating-system account, so it can only read its own credentials and nobody's admin keys.
  • Enforce allowlists on the server, not just in instructions: which domains each assistant may send to and accept mail from, with DMARC required on incoming mail so senders can't be spoofed.
  • Treat incoming mail as untrusted information, never as instructions.
  • Write a short policy: who approves outgoing mail, no deleting without the owner's say-so, and which channel is the official record.

14Lessons and pitfalls

  • Settings changes in the mail server often need a restart before they take effect.
  • Sign-in loops on the admin page usually mean the configured public URL doesn't match the address you're browsing to.
  • Port conflicts: other software on the host (for example virtualization tools) may already use port 443; publish the web admin on another port.
  • Docker Desktop on Windows can hide real client addresses, which defeats per-IP blocking, and Windows Firewall may silently block published ports. A Linux host avoids both.
  • WireGuard inside Docker Desktop may not be able to load its automatic firewall rules; disable automatic routing and add your own routes.
  • Spam blocklists refuse lookups made through public DNS resolvers. Give the mail server its own recursive resolver, or use a provider key, or those checks are silently skipped.
  • Binary data and PowerShell pipes don't mix; compute certificate hashes in bash.
  • Lookalike characters in generated passwords cause failed logins; always copy and paste.
  • A phone mail app's "Repair" button can change correct settings; cancel instead.

15Order of operations

  1. Domain on Cloudflare, DNSSEC on, API token made.
  2. Check port 25 and choose home-only or front-door.
  3. Install the mail server and finish setup.
  4. Certificates via DNS and automatic DKIM.
  5. Create mailboxes (and assistant allowlists if needed).
  6. Router: fixed IP, local DNS, firewall; phones on Wi-Fi.
  7. VPN for phones away from home.
  8. Front door and split routing, if needed.
  9. MX and A records; test receiving.
  10. Sending service, SPF and DMARC; test sending.
  11. Encrypted backups and a restore test.
  12. DANE and MTA-STS; later tighten DMARC and enforce MTA-STS.