Self-hosting a mail server — most people will say “just use Google Workspace and save yourself the headache.” But if you need full control over internal email, compliance with data residency requirements, or simply want to understand how email systems actually work under the hood — building your own is still a skill worth having.
I’ve set up mail servers in production environments multiple times. The most painful one was when CentOS 8 reached EOL and I had to urgently migrate 5 servers within a week — including 2 running mail. The biggest lesson from that experience: Dovecot + Postfix configurations need to be meticulously documented, because migrating a mail server without detailed config records is a genuine nightmare.
Comparing Approaches: Choosing the Right MTA and MDA
Approach 1: Postfix Only (SMTP only)
Postfix is an MTA (Mail Transfer Agent) — it handles only sending and receiving mail between servers. If you just need to relay mail from applications (WordPress sending notification emails, cronjob alerts), Postfix alone is sufficient.
- Pros: Simple, fewer components, easier to debug
- Cons: No IMAP/POP3 protocol — users cannot use mail clients (Outlook, Thunderbird) to read mail directly
Approach 2: Dovecot + Postfix (the standard for a real mail server)
Postfix handles SMTP for sending and receiving, while Dovecot takes care of IMAP/POP3 so mail clients can connect and read mail. This is the most widely used combination in the industry — from shared hosting to enterprise environments.
- Pros: Full-featured, high performance, strong security, extensive documentation
- Cons: Requires configuring multiple layers — especially the SELinux side on CentOS, which tends to trip up beginners
Approach 3: All-in-one Stack (iRedMail, Mailcow, Mailu)
These stacks bundle Postfix + Dovecot + SpamAssassin + Roundcube + web admin into a single install script or Docker Compose setup.
- Pros: Quick setup, web management UI, built-in anti-spam
- Cons: Hard to customize deeply, difficult to debug when things go wrong due to too many layers — and often conflicts with CentOS’s default SELinux configuration
Analysis: Why Choose the Manual Approach on CentOS Stream 9?
For a mail server serving fewer than 50 users or a learning environment, manually setting up Dovecot + Postfix helps you understand every layer — especially SELinux on CentOS, which tends to be a “black box” for newcomers. When something goes wrong, you’ll know exactly where to look.
I chose CentOS Stream 9 after my migration experience from CentOS 8. Stream 9 is more stable than its reputation suggests — mail servers don’t need bleeding-edge packages, and RHEL compatibility is a major advantage if you ever need enterprise support down the line.
Step-by-Step Deployment Guide
Step 1: Install Postfix and Dovecot
# Update the system first
sudo dnf update -y
# Install Postfix and Dovecot
sudo dnf install -y postfix dovecot mailx
# Enable and start services
sudo systemctl enable --now postfix
sudo systemctl enable --now dovecot
Step 2: Configure Postfix
The main configuration file is /etc/postfix/main.cf. I always back it up before making changes — a habit formed from the CentOS 8 migration incident.
sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.bak
Add or modify the following parameters in /etc/postfix/main.cf:
# Mail server domain
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
# Listen on all interfaces
inet_interfaces = all
inet_protocols = ipv4
# Domains to receive mail for
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
# Maildir format (compatible with Dovecot)
home_mailbox = Maildir/
# SMTP Auth via Dovecot SASL
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
# TLS settings
smtpd_tls_cert_file = /etc/pki/tls/certs/mail.example.com.crt
smtpd_tls_key_file = /etc/pki/tls/private/mail.example.com.key
smtpd_use_tls = yes
smtpd_tls_auth_only = yes
# Basic anti-spam
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination
Step 3: Generate SSL/TLS Certificate
Use Let’s Encrypt for production. For lab/test environments, use self-signed:
# Self-signed (for test environments only)
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/pki/tls/private/mail.example.com.key \
-out /etc/pki/tls/certs/mail.example.com.crt \
-subj "/C=VN/ST=HCM/L=HoChiMinh/O=Example/CN=mail.example.com"
sudo chmod 600 /etc/pki/tls/private/mail.example.com.key
# Production: use Let's Encrypt
sudo dnf install -y certbot
sudo certbot certonly --standalone -d mail.example.com
# Cert will be at /etc/letsencrypt/live/mail.example.com/
Step 4: Configure Dovecot
Dovecot uses a directory-based configuration structure at /etc/dovecot/conf.d/ — each file handles one aspect. It’s quite clean compared to Postfix’s monolithic config.
# /etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:~/Maildir
# /etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = yes
auth_mechanisms = plain login
# /etc/dovecot/conf.d/10-ssl.conf
ssl = required
ssl_cert = </etc/pki/tls/certs/mail.example.com.crt
ssl_key = </etc/pki/tls/private/mail.example.com.key
ssl_min_protocol = TLSv1.2
Configure the SASL socket so Postfix can connect to Dovecot — add to /etc/dovecot/conf.d/10-master.conf:
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
Step 5: Handle SELinux — The Most Commonly Skipped Part
This is the most painful part of setting up a mail server on CentOS. SELinux blocks many things that newcomers don’t anticipate, and the failures are often silent — the service runs but connections don’t go through.
Golden rule: Never run setenforce 0 to “fix” issues in production. Use audit2allow to create the proper policy.
# Check if SELinux is blocking anything
sudo ausearch -m avc -ts recent | grep -E 'dovecot|postfix'
# Booleans to enable
sudo setsebool -P allow_postfix_local_write_mail_spool 1
# If Dovecot needs to access files outside the default directory
sudo restorecon -Rv /etc/pki/tls/
sudo restorecon -Rv /var/spool/postfix/
# Create a custom policy from AVC denials (the most powerful approach)
sudo ausearch -m avc -ts recent | audit2allow -M postfix_dovecot_local
sudo semodule -i postfix_dovecot_local.pp
# Verify SELinux is still Enforcing after fixing
getenforce
Pro tip from the CentOS 8 migration: I exported audit logs from the old server, ran audit2allow to pre-generate the policy, then imported it into the new server before restarting services — saved an entire afternoon of SELinux debugging.
Step 6: Open Ports in firewalld
# SMTP
sudo firewall-cmd --permanent --add-service=smtp # port 25
sudo firewall-cmd --permanent --add-service=smtps # port 465
sudo firewall-cmd --permanent --add-service=smtp-submission # port 587
# IMAP and POP3
sudo firewall-cmd --permanent --add-service=imap # port 143
sudo firewall-cmd --permanent --add-service=imaps # port 993
sudo firewall-cmd --permanent --add-service=pop3 # port 110
sudo firewall-cmd --permanent --add-service=pop3s # port 995
sudo firewall-cmd --reload
sudo firewall-cmd --list-services
Step 7: Create User and Test
# Create mail user (no login shell needed)
sudo useradd -m -s /sbin/nologin mailuser
sudo passwd mailuser
# Create Maildir structure
sudo -u mailuser mkdir -p /home/mailuser/Maildir/{cur,new,tmp}
# Restart services
sudo systemctl restart postfix dovecot
# Send a local test email
echo "Test content" | mail -s "Test Subject" [email protected]
# Check if mail arrived
sudo ls -la /home/mailuser/Maildir/new/
Verifying IMAP and SMTP Connections
# Manual IMAP test
telnet localhost 143
# After connecting:
a LOGIN mailuser yourpassword
b SELECT INBOX
c LOGOUT
# Test SMTP with TLS (check SASL auth)
openssl s_client -connect mail.example.com:587 -starttls smtp
Common Errors and How to Fix Them
- Postfix not receiving external mail: Check
mydestinationandinet_interfaces = all— this parameter is commonly forgotten - Dovecot rejecting login despite correct password: 90% of the time it’s SELinux. Run
ausearch -m avc -ts recentbefore debugging anything else - Postfix logs "SASL authentication failure": Verify the socket path in
main.cf(private/auth) matches Dovecot’s10-master.conf - Mail client reports SSL error: Expected with self-signed certificates — manually trust it or use Let’s Encrypt for production
- Permission denied on Maildir: Run
restorecon -Rv /home/mailuser/to fix the SELinux context
Pre-Production Checklist
systemctl status postfix dovecot— both active with no warnings- Ports 25, 587, 993, 995 are listening:
ss -tlnp | grep -E '25|587|993|995' - SELinux is Enforcing:
getenforcemust outputEnforcing - Send/receive mail test successful via telnet
- Thunderbird connection via IMAPS port 993 successful
- Clean logs:
sudo tail -f /var/log/maillogshows no recurring errors - SPF/DKIM/DMARC configured in DNS (required to avoid spam folders)
A manual mail server on CentOS Stream 9 isn’t something you “plug in and it just works” — but once you understand every layer, from SASL handshakes to SELinux policies, you’ll be able to debug any email issue that comes up. And more importantly, when you’re forced into an emergency migration like I was, you’ll know exactly what needs to be backed up and documented.

