Deploy Stalwart Mail Server on a VPS
All-in-one Rust mail server — SMTP, IMAP4, JMAP, POP3, CalDAV/CardDAV, ManageSieve, built-in spam filter, and ACME TLS in a single binary with one config file.
At a Glance
| Project | Stalwart Mail Server |
| License | AGPL v3 |
| Recommended Plan | RamNode Cloud VPS 2 GB+ (4 GB+ for dozens of users) |
| OS | Ubuntu 24.04 LTS |
| Storage Backend | RocksDB (default), PostgreSQL optional |
| Estimated Setup Time | 60–90 minutes |
Why RamNode for mail
Two things kill self-hosted mail before it starts: blocked outbound port 25 and dirty IP space. Most major clouds (AWS, GCP, Azure, DO new accounts) block 25 by default. RamNode generally allows port 25 and runs clean IP space — open a quick ticket if 25 is restricted on your specific VPS.
- RamNode VPS with Ubuntu 24.04 LTS, 2 GB+ RAM, 40 GB+ disk
- A domain you control with DNS access
- Outbound port 25 verified:
nc -zv gmail-smtp-in.l.google.com 25 - PTR (reverse DNS) on your IPv4 set to
mail.example.com
Server Preparation
apt update && apt full-upgrade -y
hostnamectl set-hostname mail.example.com
echo "127.0.1.1 mail.example.com mail" >> /etc/hosts
timedatectl set-timezone America/New_York
adduser --gecos "" sysadmin
usermod -aG sudo sysadmin
mkdir -p /home/sysadmin/.ssh
cp ~/.ssh/authorized_keys /home/sysadmin/.ssh/
chown -R sysadmin:sysadmin /home/sysadmin/.ssh
chmod 700 /home/sysadmin/.ssh
chmod 600 /home/sysadmin/.ssh/authorized_keysPermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yessystemctl restart ssh
sudo apt install -y curl wget gnupg ca-certificates ufw fail2ban htop dnsutils net-toolsConfigure DNS Records
Set these before installing — the WebAdmin certificate provisioning needs them resolving.
A mail.example.com -> YOUR_VPS_IPV4
AAAA mail.example.com -> YOUR_VPS_IPV6 (if available)
MX example.com -> 10 mail.example.com.
TXT example.com -> v=spf1 mx ~all
TXT _dmarc.example.com -> v=DMARC1; p=quarantine; rua=mailto:postmaster@example.comDKIM, MTA-STS, TLS-RPT, and autoconfig records get added later once Stalwart generates them. Verify propagation with dig +short mail.example.com.
Install Stalwart
sudo mkdir -p /opt/stalwart
cd /opt/stalwart
sudo curl --proto '=https' --tlsv1.2 -sSf https://get.stalw.art/install.sh -o install.sh
sudo sh install.sh /opt/stalwartChoose: All-in-one server type, RocksDB storage, Filesystem blob storage, spam filter and WebAdmin enabled. Capture the generated admin password immediately — it is not displayed again.
sudo systemctl status stalwart
sudo ss -tlnp | grep stalwart # 25, 465, 587, 993, 995, 4190, 8080Configure the Firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 25/tcp # SMTP
sudo ufw allow 465/tcp # SMTPS
sudo ufw allow 587/tcp # Submission
sudo ufw allow 993/tcp # IMAPS
sudo ufw allow 995/tcp # POP3S (optional)
sudo ufw allow 4190/tcp # ManageSieve
sudo ufw allow 80/tcp # ACME
sudo ufw allow 443/tcp # WebAdmin
sudo ufw enableFirst Login and ACME TLS
Visit http://YOUR_VPS_IPV4:8080, sign in as admin, change the password immediately under Manage → Accounts → admin → Edit.
Then go to Settings → Server → TLS → ACME Providers → Create new:
- ID:
letsencrypt - Directory:
https://acme-v02.api.letsencrypt.org/directory - Challenge: HTTP-01
- Domains:
mail.example.com,autoconfig.example.com,autodiscover.example.com,mta-sts.example.com
sudo journalctl -u stalwart -f | grep -i acme
sudo systemctl restart stalwartSwitch the HTTP listener to 443 with the new cert (Settings → Server → Listeners). WebAdmin then lives at https://mail.example.com/admin.
Create Your Domain and First Account
In WebAdmin go to Manage → Directory → Domains and add example.com. Stalwart generates a DKIM keypair and shows the DNS record to publish.
stalwart._domainkey.example.com TXT v=DKIM1; k=rsa; p=...
_mta-sts.example.com TXT v=STSv1; id=20260101000000
_smtp._tls.example.com TXT v=TLSRPTv1; rua=mailto:tlsrpt@example.com
autoconfig.example.com CNAME mail.example.com.
autodiscover.example.com CNAME mail.example.com.
mta-sts.example.com CNAME mail.example.com.Then create your first user under Manage → Directory → Accounts → Create new with a strong password and a quota (e.g. 5 GB). Thunderbird will auto-detect IMAP/SMTP via the autoconfig CNAME.
Verify Outbound Authentication
Send a test message to test@mail-tester.com and aim for 10/10. Common deductions:
- Missing PTR — open a RamNode ticket to set rDNS
- DKIM mismatch — DNS provider mangled the long base64 value
- SPF —
v=spf1 mx ~allworks as long as MX = sender
dig +short TXT stalwart._domainkey.example.comConfigure Spam Filtering
- Bayes: enable auto-learning under Settings → Spam filter → Bayes classifier
- RBLs: defaults are conservative; add
zen.spamhaus.org,dbl.spamhaus.org,bl.spamcop.netas needed - Greylisting: highly effective against botnets — enable for personal/low-volume mail, leave off for transactional
Hardening
# /etc/fail2ban/jail.d/stalwart.conf
[stalwart]
enabled = true
backend = systemd
filter = stalwart
maxretry = 5
findtime = 600
bantime = 86400
journalmatch = _SYSTEMD_UNIT=stalwart.service[Definition]
failregex = .*authentication failed.*remote_ip="<HOST>".*
.*invalid credentials.*remote_ip="<HOST>".*
ignoreregex =sudo systemctl restart fail2ban
sudo fail2ban-client status stalwartAlso configure auth-failure bans inside Stalwart (Settings → Server → Security: max 5 failures, ban 24 h), and restrict /admin by source IP if remote admin is not needed.
Backups
sudo apt install -y restic
sudo mkdir /etc/restic
sudo chmod 700 /etc/restic
sudo openssl rand -base64 48 | sudo tee /etc/restic/password
sudo chmod 600 /etc/restic/password
sudo restic -r sftp:backup@backup.example.net:/backups/mail \
--password-file /etc/restic/password init#!/bin/bash
set -euo pipefail
REPO="sftp:backup@backup.example.net:/backups/mail"
PASS="/etc/restic/password"
restic -r "$REPO" --password-file "$PASS" backup \
/opt/stalwart/data \
/opt/stalwart/etc
restic -r "$REPO" --password-file "$PASS" forget \
--keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prunesudo chmod +x /usr/local/sbin/stalwart-backup.sh
echo "30 3 * * * root /usr/local/sbin/stalwart-backup.sh >> /var/log/stalwart-backup.log 2>&1" \
| sudo tee /etc/cron.d/stalwart-backupAn untested backup is a hopeful guess. Run a restore at least once before relying on it.
Common Issues
- Gmail/Microsoft reject, others accept: rDNS or DMARC alignment —
dig -x YOUR_IP - WebAdmin 404 after install: outbound HTTPS to
github.comblocked — bundle didn't download - ACME "connection refused" on 80: something else listening, or VPS-level firewall blocks 80
- STARTTLS required on 587: client connecting plain — enable STARTTLS or use 465
- RocksDB OOM under load: bump to 4 GB plan or limit write-buffer in
[storage.rocksdb]
