Mail Server
    Open Source

    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

    ProjectStalwart Mail Server
    LicenseAGPL v3
    Recommended PlanRamNode Cloud VPS 2 GB+ (4 GB+ for dozens of users)
    OSUbuntu 24.04 LTS
    Storage BackendRocksDB (default), PostgreSQL optional
    Estimated Setup Time60–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
    1

    Server Preparation

    Update, hostname, admin user
    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_keys
    Harden SSH (/etc/ssh/sshd_config)
    PermitRootLogin no
    PasswordAuthentication no
    PubkeyAuthentication yes
    Install supporting packages
    systemctl restart ssh
    sudo apt install -y curl wget gnupg ca-certificates ufw fail2ban htop dnsutils net-tools
    2

    Configure DNS Records

    Set these before installing — the WebAdmin certificate provisioning needs them resolving.

    Required DNS records
    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.com

    DKIM, MTA-STS, TLS-RPT, and autoconfig records get added later once Stalwart generates them. Verify propagation with dig +short mail.example.com.

    3

    Install Stalwart

    Run the interactive installer
    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/stalwart

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

    Verify the service
    sudo systemctl status stalwart
    sudo ss -tlnp | grep stalwart   # 25, 465, 587, 993, 995, 4190, 8080
    4

    Configure the Firewall

    UFW rules
    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 enable
    5

    First 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
    Trigger issuance and watch logs
    sudo journalctl -u stalwart -f | grep -i acme
    sudo systemctl restart stalwart

    Switch the HTTP listener to 443 with the new cert (Settings → Server → Listeners). WebAdmin then lives at https://mail.example.com/admin.

    6

    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.

    Publish these DNS records exactly as shown
    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.

    7

    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
    • SPFv=spf1 mx ~all works as long as MX = sender
    Compare DKIM record
    dig +short TXT stalwart._domainkey.example.com
    8

    Configure 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.net as needed
    • Greylisting: highly effective against botnets — enable for personal/low-volume mail, leave off for transactional
    9

    Hardening

    fail2ban for Stalwart auth failures
    # /etc/fail2ban/jail.d/stalwart.conf
    [stalwart]
    enabled = true
    backend = systemd
    filter = stalwart
    maxretry = 5
    findtime = 600
    bantime = 86400
    journalmatch = _SYSTEMD_UNIT=stalwart.service
    /etc/fail2ban/filter.d/stalwart.conf
    [Definition]
    failregex = .*authentication failed.*remote_ip="<HOST>".*
                .*invalid credentials.*remote_ip="<HOST>".*
    ignoreregex =
    Apply
    sudo systemctl restart fail2ban
    sudo fail2ban-client status stalwart

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

    10

    Backups

    Initialize a restic repo over SFTP
    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
    /usr/local/sbin/stalwart-backup.sh
    #!/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 --prune
    Schedule
    sudo 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-backup

    An 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.com blocked — 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]