This repository contains an Ansible playbook that provisions a complete, production-ready mail platform on Ubuntu 24.04. It follows the Pieter Hollander mail server guide while wrapping the workflow in reusable roles, helpful defaults, and guard rails for modern DNS, TLS, and spam-hardening standards.
For a terminal-friendly reference of the most common commands see CHEATSHEET.md.
- Postfix - SMTP server for sending and receiving email
- Dovecot - IMAP server with Sieve filtering and ManageSieve support
- Rspamd - Advanced spam filtering with DKIM signing
- PostgreSQL - Database for virtual domains, mailboxes, and aliases
- Redis - Backend for Rspamd learning data
- ✅ SPF, DKIM, and DMARC support
- ✅ DNSSEC validation
- ✅ DANE/TLSA for SMTP server validation
- ✅ MTA-STS for secure mail transport
- ✅ TLS 1.2+ with strong ciphers
- ✅ Let's Encrypt certificates with auto-renewal
- ✅ Fail2ban protection against brute force attacks
- ✅ UFW firewall configuration
- Self-learning Bayesian spam filter
- Address tagging (e.g.,
support+tag@example.com) - Email aliases and forwarding
- Server-side Sieve filtering
- Automatic mailbox folders (Sent, Drafts, Spam, etc.)
- Autoconfig for easy client setup
- Web-based Rspamd admin interface
- Ubuntu 24.04 LTS
- 2 vCPU / 2GB RAM minimum (4GB recommended)
- 20GB+ disk (depends on mailbox volume)
- Public IPv4 (IPv6 optional but supported)
- Root or passwordless sudo access
- Ansible 2.10 or later
- SSH access to the target server
- Python 3.6+ on both local machine and server
You should have already configured:
- A record:
mail.example.com→ Your server IP - MX record:
example.com→mail.example.com(priority 10)
Additional DNS records will be provided after the playbook runs.
Prefer a guided workflow? Run
./setup.sh— it installs collections, checks your configuration, encrypts the vault, and runs the playbook for you. The steps below mirror what the script does so you can run things manually or in CI.
# macOS
brew install ansible
# Ubuntu/Debian
sudo apt update && sudo apt install ansible
First, copy the domain configuration template:
```bash
cp group_vars/all/config.yml.example group_vars/all/config.ymlEdit group_vars/all/config.yml with your domain settings:
mail_domain: "example.com"
mail_hostname: "mail.example.com"
email_accounts:
- email: "support@example.com"
name: "Support Team"
- email: "admin@example.com"
name: "Admin"Then edit group_vars/all/vars.yml:
# Update these values
mail_server_ip: "YOUR_SERVER_IP"
mail_server_ipv4: "YOUR_SERVER_IPV4"
mail_server_ipv6: "" # Leave empty if not using IPv6
mail_server_user: "root"Copy the example vault file:
cp group_vars/all/vault.yml.example group_vars/all/vault.ymlEdit group_vars/all/vault.yml and set strong passwords:
postgresql_vmail_password: "STRONG_PASSWORD_HERE"
postgresql_postgres_password: "STRONG_PASSWORD_HERE"
# Generate with: docker run --rm rspamd/rspamd rspamadm pw
# Or on server after install: rspamadm pw
rspamd_controller_password: "$2$..."
email_passwords:
"support@example.com": "STRONG_PASSWORD_HERE"
"admin@example.com": "STRONG_PASSWORD_HERE"
rspamd_web_user: "admin"
rspamd_web_password: "STRONG_PASSWORD_HERE"Encrypt the vault file:
ansible-vault encrypt group_vars/all/vault.ymlEdit inventory.yml so the host name matches your real mail server (replace the placeholder mail.example.com shown in the file comment). The rest of the connection details can stay templated to pull from group_vars/all/vars.yml:
all:
children:
mailservers:
hosts:
mail.example.com: # must match your DNS A record
ansible_host: "{{ mail_server_ip | default(mail_server_ipv4) }}"
ansible_user: "{{ mail_server_user | default('root') }}"
ansible_python_interpreter: "{{ ansible_python_interpreter | default('/usr/bin/python3') }}"ansible-playbook -i inventory.yml playbook.yml --ask-vault-passEnter your vault password when prompted.
| File | Purpose | Key values |
|---|---|---|
group_vars/all/config.yml |
Mailbox + alias definitions | mail_domain, mail_hostname, email_accounts, email_aliases, optional additional_domains |
group_vars/all/vars.yml |
Server connectivity + feature toggles | mail_server_ip, mail_server_user, rspamd_spam_threshold, fail2ban_whitelist, etc. |
group_vars/all/vault.yml |
Secrets encrypted via Ansible Vault | DB passwords, mailbox passwords, DKIM/Rspamd credentials |
inventory.yml |
Host definitions | Ensure hostnames line up with DNS records |
Whenever you change config.yml or vault.yml, re-run the relevant role(s). Example: mailbox updates only need --tags postgresql.
After a successful run the playbook prints the records you need, but the full set is below for planning:
| Type | Name | Value |
|---|---|---|
| A | mail.example.com |
Server IPv4 |
| A | smtp.example.com |
Server IPv4 |
| A | imap.example.com |
Server IPv4 |
| A | autoconfig.example.com |
Server IPv4 |
| A | mta-sts.example.com |
Server IPv4 |
| A | rspamd.example.com |
Server IPv4 |
| MX | example.com |
mail.example.com priority 10 |
| TXT | example.com |
"v=spf1 mx ~all" |
| TXT | _dmarc.example.com |
"v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com" |
| TXT | _mta-sts.example.com |
"v=STSv1; id=<timestamp>" |
| TXT | default._domainkey.example.com |
DKIM public key generated by Rspamd |
| TXT | _smtp._tls.example.com |
(Optional) SMTP TLS reporting |
| TLSA | _25._tcp.mail.example.com |
3 1 1 <hash> |
| TLSA | _587._tcp.mail.example.com |
3 1 1 <hash> |
Generate the TLSA hash on the server:
echo "3 1 1 $(openssl x509 -in /etc/lego/certificates/mail.example.com.crt -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary | xxd -p -u -c 32)"- Rspamd UI:
https://rspamd.example.com(credentials from the vault) - Autoconfig endpoint:
https://autoconfig.example.com/mail/config-v1.1.xml - MTA-STS policy:
https://mta-sts.example.com/.well-known/mta-sts.txt
| Protocol | Host | Port | Security | Notes |
|---|---|---|---|---|
| IMAP | mail.example.com |
993 | SSL/TLS | Username = full email |
| IMAP | mail.example.com |
143 | STARTTLS | |
| SMTP submission | mail.example.com |
587 | STARTTLS | Auth required |
| ManageSieve | mail.example.com |
4190 | STARTTLS | Use for Sieve rules |
systemctl status postfix dovecot rspamd redis-server postgresql fail2ban
tail -f /var/log/mail.log
journalctl -u postfix -f
journalctl -u dovecot -f
journalctl -u rspamd -fSend a real-world test:
echo "Test email body" | mail -s "Test Subject" you@example.net# group_vars/all/config.yml
email_accounts:
- email: "newuser@example.com"
name: "New User"# group_vars/all/vault.yml
email_passwords:
"newuser@example.com": "STRONG_PASSWORD"ansible-playbook -i inventory.yml playbook.yml --tags postgresql --ask-vault-passemail_aliases:
- alias: "sales@example.com"
destination: "team@example.com"Re-run with --tags postgresql.
doveadm pw -s SHA512-CRYPT # produce hash
sudo -u postgres psql vmail \
-c "UPDATE accounts SET password='HASH' WHERE email='user@example.com';"/usr/local/bin/renew-certificates.sh- Move spam into the "Spam" IMAP folder to learn spam
- Move false positives out of "Spam" to teach ham
systemctl status postfix
journalctl -u postfix -n 50
telnet mail.example.com 587
doveadm auth test user@example.comVerify DNS + reverse DNS, then check the Postfix queue (mailq, postqueue -p).
dig example.com MX
ss -tlnp | grep :25
tail -f /var/log/mail.logLook for firewall blocks or upstream providers blocking port 25.
systemctl status rspamd
rspamc stat
rspamc < sample.eml
journalctl -u rspamd -n 100Adjust thresholds in group_vars/all/vars.yml (rspamd_spam_threshold) and redeploy rspamd.
openssl x509 -in /etc/lego/certificates/mail.example.com.crt -text -noout
/usr/local/bin/renew-certificates.shEnsure DNS A/AAAA records resolve before requesting certificates.
sudo -u postgres psql vmail
\dt
SELECT email, enabled FROM accounts;Repair permissions:
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO vmail;- Firewall: UFW allows only SSH, SMTP (25/587), IMAP (143/993), LMTP, ManageSieve, HTTPS for ACME endpoints. Layer it with your provider’s security groups.
- Fail2ban:
fail2ban-client status/fail2ban-client status postfix - Monitoring: Track disk usage (
/var/vmail), certificate expiry, service uptime, and login failures. - Backups: Keep copies of
/var/vmail,/var/lib/rspamd,/var/lib/redis,/etc/lego,/etc/postfix,/etc/dovecot, and thevmailPostgreSQL DB (pg_dump vmail). - Performance: Use
htop,ss -s, andmailqto monitor load, sockets, and queues.
- mail-tester.com — overall deliverability
- internet.nl/mail — DNSSEC/DANE/MTA-STS validation
- MXToolbox — MX and DNS diagnostics
- dmarcian Inspector — DMARC validation
For issues with this automation:
Issues and PRs are welcome! If you hit deployment problems:
- Re-run with
ansible-playbook ... -vvv - Inspect the generated
/tmp/ansible-*.logfiles - Check the service logs listed above
This project is provided as-is for operating your own mail server. See the repository license for details.
Based on the outstanding research and documentation by Pieter Hollander: https://pieterhollander.nl/post/mailserver/