Can you self-host email at all
Before any install: deliverability is the hard part. Big mail providers (Gmail, Outlook, Yahoo) accept inbound from any properly-DNS'd server, but flag outbound from residential IP ranges and from new domains. Three pre-checks:
- Port 25 outbound must work. Most residential ISPs block it; cloud VPSes mostly allow it but some require unblocking via a ticket.
- The IP can't be on a major spam blocklist. Check Spamhaus, Barracuda, SORBS before claiming it.
- Reverse DNS (PTR) must match your hostname. Most VPS providers expose a control-panel setting; some require a ticket.
If any of those is no, plan to use a transactional relay (Postmark, Mailgun, AWS SES) as the outbound smarthost. Stalwart handles that case cleanly.
Install
# Debian / Ubuntu
SV=0.10.0 # check stalw.art for current
curl -L -o /tmp/stalwart.deb \
"https://github.com/stalwartlabs/mail-server/releases/download/v${SV}/stalwart-mail_${SV}_amd64.deb"
sudo apt install /tmp/stalwart.deb
# Or container
docker pull stalwartlabs/mail-server:latest
docker volume create stalwart-data
docker run -d \
--name stalwart-mail \
--restart unless-stopped \
-p 25:25 -p 465:465 -p 587:587 -p 143:143 -p 993:993 \
-p 4190:4190 -p 8080:443 \
-v stalwart-data:/opt/stalwart-mail \
stalwartlabs/mail-server:latest
First run prompts for the install wizard at http://<host>:8080/admin — pick storage backend (default: embedded RocksDB, fine up to thousands of users), set admin credentials, choose domains.
DNS records
Email DNS is the unforgiving part. For domain example.com with mail server mail.example.com:
; MX record — "who handles mail for example.com"
example.com. IN MX 10 mail.example.com.
; A / AAAA for the mail server
mail.example.com. IN A 203.0.113.10
mail.example.com. IN AAAA 2001:db8::10
; SPF — "only this server can send mail for example.com"
example.com. IN TXT "v=spf1 ip4:203.0.113.10 ip6:2001:db8::10 -all"
; DKIM — Stalwart generates the key; copy its public part here
; The "selector" is anything you like; "stalwart" is conventional
stalwart._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=<long-public-key>"
; DMARC — how receivers should treat failures
_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com; aspf=s; adkim=s"
; (Optional but increasingly important) MTA-STS — TLS enforcement
_mta-sts.example.com. IN TXT "v=STSv1; id=2025090100"
mta-sts.example.com. IN CNAME mail.example.com.
; Reverse DNS (in the IP provider's interface, NOT yours)
; 203.0.113.10 PTR mail.example.com.
Stalwart's admin UI under Domains → example.com → DNS gives the exact records to copy/paste, with a "validate" button that re-resolves and reports red/green per record.
TLS
Mail TLS terminates inside Stalwart on ports 465/587/993. Two options:
- Let Stalwart get its own cert via ACME. Admin → Settings → TLS → enable ACME, set the contact email. Stalwart talks to Let's Encrypt directly — no certbot needed.
- Reuse an existing cert (e.g. one Caddy or nginx already manages). Drop the cert + key paths into the TLS config; reload on rotation.
Add a user mailbox
# Via CLI
sudo stalwart-cli account create amir@example.com \
--display-name "Amir" \
--password <password> \
--quota 5G
# Or via the web admin (Accounts → Add)
Configure a mail client (Thunderbird, Apple Mail, K-9 Mail) with:
- Incoming: IMAP on
mail.example.com:993(TLS) OR JMAP athttps://mail.example.com/jmap - Outgoing: SMTP submission on
mail.example.com:465(TLS) or 587 (STARTTLS) - Username:
amir@example.com - Sieve filtering on
mail.example.com:4190
Anti-spam
Stalwart's built-in anti-spam combines:
- SPF / DKIM / DMARC verification on inbound
- Bayesian classifier per user, learning from "mark as spam" / "not spam"
- DNS block lists (Spamhaus, SpamCop, etc.)
- Greylisting (optional) — reject the first delivery attempt, accept the retry
- SpamAssassin-style rules with the bundled rule set
For virus scanning, enable the ClamAV plugin (Stalwart talks to a local clamd socket).
Sieve filtering
Sieve scripts (per-user) auto-file incoming mail. Stalwart includes the standard Sieve interpreter plus the ManageSieve protocol on port 4190 for client editing:
require ["fileinto", "envelope"];
if address :is "from" "github@notifications.github.com" {
fileinto "INBOX/GitHub";
stop;
}
if anyof (header :contains "List-ID" "discuss.example.com",
header :contains "Subject" "[announcements]") {
fileinto "INBOX/Mailing Lists";
}
Apple Mail and Thunderbird have ManageSieve plugins (sieve.client / sieve add-on); Roundcube has one built in. Stalwart can also serve a webmail (it does not bundle one in-tree; pair with Roundcube or Snappymail).
Outbound relaying through a smarthost
If port 25 outbound is blocked (residential IP, some VPS providers), route outbound through a transactional service:
Admin → SMTP → Outbound → Relay
Host: smtp.postmarkapp.com (or smtp.mailgun.org, etc.)
Port: 587
Auth: STARTTLS, username/password
Inbound still arrives on your server directly via port 25; outbound traffic carries the relay's reputation and the relay does the DKIM-signing of its own headers.
Backups
One directory holds everything: configuration, the user database, all mailboxes (Maildir-like Stalwart format), DKIM keys, ACME certs. Snapshot it (filesystem snapshot or restic) on a nightly schedule. The bundled stalwart-cli backup command writes a portable archive too.
DKIM keys are the one piece worth a special note: lose them, every recipient flags your email as forged for whatever cache lifetime the DKIM records have. Keep an out-of-band copy of data/etc/dkim/.
Migrating from another server
Stalwart's import tool can read Maildir, mbox, or IMAP source servers and copy mailboxes into the local storage:
stalwart-cli import imap \
--source imap.old.example.com \
--user-source-creds amir@example.com:<oldpass> \
--target-user amir@example.com
Combined with overlapping MX records during the transition, this is the standard migration path. Run with both inboxes active for a week, switch the MX, retire the old server.
When self-hosting email is the wrong call
For a small business or family where any-loss-of-email is a real-world problem, paying $5–10 per mailbox per month (Fastmail, Migadu, ProtonMail, Microsoft 365) buys vastly more deliverability than self-hosting. Self-hosting makes sense for technical curiosity, fine-grained control, jurisdictional / privacy concerns, or zero-cost-per-mailbox at scale — not because "it must be cheaper." It rarely is, once an outage costs you a real reply.