Running your own mail server on the Enterprise Linux family is still one of the most useful skills a sysadmin can have, whether you need an internal relay for application alerts or a full mailbox server for a small domain. The standard stack on Red Hat Enterprise Linux and its rebuilds is Postfix as the SMTP server (MTA) and Dovecot as the IMAP server and SASL authentication provider. Both ship in the base AppStream repository, so you need nothing from third-party repos for the core setup.
This guide builds a single-host mail server for one domain with local system users, TLS everywhere, authenticated submission on port 587 for mail clients, IMAPS on port 993, and LMTP delivery from Postfix into Dovecot. The commands were checked against the Red Hat Enterprise Linux 10 Deploying mail servers documentation and the Dovecot 2.3 docs, and they apply to RHEL 9 and 10, AlmaLinux, Rocky Linux, Oracle Linux and CentOS Stream. Where EL9 and EL10 differ, the difference is called out.
What you will build
- Mail host:
mail.example.comat192.168.1.40, mail domainexample.com - Postfix receives mail for example.com on port 25 and hands it to Dovecot over LMTP
- Users send mail through Postfix on port 587 (STARTTLS + SASL authentication handled by Dovecot)
- Users read mail with IMAPS on port 993, stored as Maildir in each user home directory
- SELinux enforcing and firewalld enabled throughout
Replace the hostname, IP address and domain with your own everywhere they appear.
Package versions by release
| Release | Postfix | Dovecot | Default Postfix map type |
|---|---|---|---|
| RHEL 9.x, AlmaLinux 9, Rocky Linux 9, Oracle Linux 9 | 3.5.25 | 2.3.16 | hash (Berkeley DB); lmdb available via postfix-lmdb |
| RHEL 10.x, AlmaLinux 10, Rocky Linux 10, Oracle Linux 10 | 3.8.5 | 2.3.21 | lmdb (Berkeley DB removed) |
| CentOS Stream 9 / 10 | Tracks the next 9.x / 10.x minor | Tracks the next minor | Same as EL9 / EL10 |
The biggest practical difference is the lookup table format. RHEL 10 no longer ships the Berkeley DB library, so Postfix cannot open hash: or btree: tables and uses LMDB by default. This guide uses lmdb: on both releases so the same configuration works on EL9 and EL10 and survives a future in-place upgrade.
Prerequisites
- A minimal EL9 or EL10 install with a static IP and root or sudo access.
- DNS records: an
Arecord formail.example.com, anMXrecord forexample.compointing tomail.example.com, and a matching reverse (PTR) record for the public IP. Many receiving servers reject mail from hosts without forward-confirmed reverse DNS. - An SPF record such as
v=spf1 mx -allas a TXT record onexample.com. - Accurate time (chrony is enabled by default) so TLS certificate validation works.
- Outbound TCP 25 allowed by your network or cloud provider. Many clouds block it; if yours does, you will need a relayhost (covered in troubleshooting).
- A TLS certificate for mail.example.com from a public CA (Let’s Encrypt is shown below) or your internal CA.
Set the hostname first, because Postfix derives its defaults from it:
sudo hostnamectl set-hostname mail.example.com
hostname -f
Step 1: Install Postfix and Dovecot
On every release the packages come from AppStream. s-nail provides the mail command used for testing.
# EL9 (RHEL 9, AlmaLinux 9, Rocky Linux 9, Oracle Linux 9, CentOS Stream 9)
sudo dnf -y install postfix postfix-lmdb dovecot s-nail
# EL10 (postfix-lmdb is pulled in automatically as a dependency)
sudo dnf -y install postfix dovecot s-nail
If Sendmail is installed (possible on EL9; it was removed from RHEL 10), remove it so Postfix owns the /usr/sbin/sendmail alternative:
sudo dnf -y remove sendmail
sudo alternatives --display mta
Oracle Linux users: the packages are in the ol9_appstream / ol10_appstream repositories, which are enabled by default. The kernel choice (UEK or RHCK) makes no difference to this setup.
Step 2: Get a TLS certificate
Any certificate whose subject or SAN matches mail.example.com works. To use Let’s Encrypt, install certbot from EPEL and request a certificate in standalone mode (temporarily open port 80):
# AlmaLinux / Rocky Linux / CentOS Stream
sudo dnf -y install epel-release
# Oracle Linux 9 (use oracle-epel-release-el10 on OL10)
sudo dnf -y install oracle-epel-release-el9
# RHEL: enable CodeReady Builder and install the epel-release RPM from the Fedora EPEL docs
sudo dnf -y install certbot
sudo firewall-cmd --add-service=http
sudo certbot certonly --standalone -d mail.example.com
sudo firewall-cmd --remove-service=http
Rather than pointing Postfix and Dovecot into /etc/letsencrypt, copy the files into the standard RHEL PKI directories, which already carry the correct SELinux label (cert_t). A certbot deploy hook keeps the copies current after each renewal:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/mail.sh >/dev/null <<'EOF'
#!/bin/bash
install -m 0644 /etc/letsencrypt/live/mail.example.com/fullchain.pem /etc/pki/tls/certs/mail.example.com.crt
install -m 0600 /etc/letsencrypt/live/mail.example.com/privkey.pem /etc/pki/tls/private/mail.example.com.key
restorecon /etc/pki/tls/certs/mail.example.com.crt /etc/pki/tls/private/mail.example.com.key
systemctl reload postfix dovecot 2>/dev/null || true
EOF
sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/mail.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/mail.sh
sudo systemctl enable --now certbot-renew.timer
Step 3: Configure Postfix
Use postconf -e to edit /etc/postfix/main.cf so every change is a single, repeatable command:
sudo postconf -e 'myhostname = mail.example.com'
sudo postconf -e 'mydomain = example.com'
sudo postconf -e 'myorigin = $mydomain'
sudo postconf -e 'inet_interfaces = all'
sudo postconf -e 'inet_protocols = all'
sudo postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain'
sudo postconf -e 'mynetworks = 127.0.0.0/8, [::1]/128'
sudo postconf -e 'message_size_limit = 26214400'
# LMDB lookup tables (default on EL10, needs postfix-lmdb on EL9)
sudo postconf -e 'default_database_type = lmdb'
sudo postconf -e 'alias_maps = lmdb:/etc/aliases'
sudo postconf -e 'alias_database = lmdb:/etc/aliases'
# Hand local mailboxes to Dovecot over LMTP
sudo postconf -e 'mailbox_transport = lmtp:unix:/var/run/dovecot/lmtp'
# TLS
sudo postconf -e 'smtpd_tls_cert_file = /etc/pki/tls/certs/mail.example.com.crt'
sudo postconf -e 'smtpd_tls_key_file = /etc/pki/tls/private/mail.example.com.key'
sudo postconf -e 'smtpd_tls_security_level = may'
sudo postconf -e 'smtp_tls_security_level = may'
sudo postconf -e 'smtpd_tls_auth_only = yes'
# SASL via Dovecot (enabled only on the submission port below)
sudo postconf -e 'smtpd_sasl_type = dovecot'
sudo postconf -e 'smtpd_sasl_path = private/auth'
sudo postconf -e 'smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination'
Port 25 stays open for server-to-server mail without authentication. Mail clients use the submission service on port 587, which requires TLS and a login. Define it in /etc/postfix/master.cf with postconf -M and postconf -P:
sudo postconf -M submission/inet='submission inet n - n - - smtpd'
sudo postconf -P 'submission/inet/syslog_name=postfix/submission'
sudo postconf -P 'submission/inet/smtpd_tls_security_level=encrypt'
sudo postconf -P 'submission/inet/smtpd_sasl_auth_enable=yes'
sudo postconf -P 'submission/inet/smtpd_client_restrictions=permit_sasl_authenticated,reject'
sudo postconf -P 'submission/inet/smtpd_relay_restrictions=permit_sasl_authenticated,reject'
sudo postconf -P 'submission/inet/milter_macro_daemon_name=ORIGINATING'
sudo postconf -M submission/inet
Point the postmaster and root aliases at a real user, then build the alias table. On EL9 this converts /etc/aliases to /etc/aliases.lmdb:
echo 'root: alice' | sudo tee -a /etc/aliases
sudo newaliases
sudo postfix check
Crypto settings: Postfix on RHEL follows the system-wide crypto policy, so TLS protocol versions and ciphers are already sane. Use update-crypto-policies --show to check the policy instead of hand-tuning cipher lists.
Step 4: Configure Dovecot
Dovecot reads /etc/dovecot/dovecot.conf plus everything in /etc/dovecot/conf.d/. Make the following edits in the files named.
/etc/dovecot/dovecot.conf: serve IMAP and LMTP only (no POP3):
protocols = imap lmtp
/etc/dovecot/conf.d/10-mail.conf: store mail as Maildir in each user home:
mail_location = maildir:~/Maildir
first_valid_uid = 1000
/etc/dovecot/conf.d/10-auth.conf: keep plaintext logins disabled outside TLS, allow the LOGIN mechanism for Outlook, and strip the domain so alice@example.com maps to the system user alice (this is also what makes LMTP deliveries find the user):
disable_plaintext_auth = yes
auth_mechanisms = plain login
auth_username_format = %Ln
/etc/dovecot/conf.d/10-ssl.conf: require TLS and use the certificate from Step 2. The leading < tells Dovecot to read the value from a file:
ssl = required
ssl_cert = </etc/pki/tls/certs/mail.example.com.crt
ssl_key = </etc/pki/tls/private/mail.example.com.key
ssl_dh = </etc/dovecot/dh.pem
ssl_min_protocol = TLSv1.2
# Generate DH parameters once (can take a few minutes)
sudo openssl dhparam -out /etc/dovecot/dh.pem 4096
/etc/dovecot/conf.d/10-master.conf: give Postfix access to the LMTP socket and create the SASL socket inside the Postfix queue directory. Edit the existing service lmtp and service auth sections so they contain:
service lmtp {
unix_listener lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
unix_listener auth-userdb {
mode = 0600
}
}
/etc/dovecot/conf.d/20-lmtp.conf: set the postmaster address used in bounces:
protocol lmtp {
postmaster_address = postmaster@example.com
}
/etc/dovecot/conf.d/15-mailboxes.conf: auto-create and subscribe the standard folders so every client finds Sent, Drafts, Junk and Trash. Add auto = subscribe to each special-use mailbox inside namespace inbox:
namespace inbox {
mailbox Drafts {
special_use = \Drafts
auto = subscribe
}
mailbox Junk {
special_use = \Junk
auto = subscribe
}
mailbox Trash {
special_use = \Trash
auto = subscribe
}
mailbox Sent {
special_use = \Sent
auto = subscribe
}
}
Check the merged configuration. doveconf -n prints only non-default settings and fails loudly on syntax errors:
sudo doveconf -n
Step 5: Open the firewall and start the services
sudo firewall-cmd --permanent --add-service=smtp --add-service=smtp-submission --add-service=imaps
sudo firewall-cmd --reload
sudo firewall-cmd --list-services
sudo systemctl enable --now dovecot postfix
sudo systemctl status postfix dovecot --no-pager
No SELinux booleans or port labels are needed for this layout: ports 25, 587 and 993 are already labeled, the SASL socket under /var/spool/postfix/private and the LMTP socket under /run/dovecot are allowed by the targeted policy, and ~/Maildir gets the mail_home_rw_t type automatically.
Step 6: Create a mail user
Mail users are regular system accounts. Give them a nologin shell so the mail server is not also a shell server:
sudo useradd -m -s /sbin/nologin alice
sudo passwd alice
Verify the mail server
Work through these checks in order. Each one isolates a single component.
1. Listening ports
sudo ss -tlnp | grep -E ':(25|587|993)\s'
2. Dovecot authentication (should print passdb: alice auth succeeded):
sudo doveadm auth test alice
sudo doveadm user alice
3. Local delivery through Postfix and LMTP:
echo "Test body" | mail -s "Local test" alice@example.com
sudo tail -n 20 /var/log/maillog
sudo ls /home/alice/Maildir/new/
A good delivery logs a line containing relay=mail.example.com[/var/run/dovecot/lmtp] and status=sent (250 2.0.0 <alice@example.com> ... Saved).
4. TLS on submission and IMAPS (check that the certificate chain verifies and the name matches):
openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com </dev/null | grep -E 'subject=|Verify return code'
openssl s_client -connect mail.example.com:993 -servername mail.example.com </dev/null | grep -E 'subject=|Verify return code'
5. Authenticated submission: install swaks from EPEL and send as alice from another machine:
swaks --server mail.example.com:587 --tls --auth LOGIN \
--auth-user alice --from alice@example.com --to you@gmail.com
6. No open relay: from a host outside mynetworks, try to relay without logging in. Postfix must answer 554 5.7.1 ... Relay access denied:
swaks --server mail.example.com:25 --from test@outside.example --to someone@gmail.com
7. Mail client: configure Thunderbird or another client with IMAP mail.example.com:993 SSL/TLS and SMTP mail.example.com:587 STARTTLS, username alice (or alice@example.com), normal password.
Troubleshooting
Postfix and Dovecot both log to /var/log/maillog through rsyslog. On minimal systems without rsyslog, use the journal: journalctl -u postfix -u dovecot -f.
| Symptom in the log | Cause and fix |
|---|---|
unsupported dictionary type: hash | Seen on EL10 or after a leapp upgrade from EL9. Replace every hash: / btree: in main.cf and master.cf with lmdb:, run postmap lmdb:/path for each table and newaliases. postconf -n | grep -E 'hash:|btree:' finds leftovers. |
SASL: Connect to private/auth failed | Dovecot is not running or the service auth listener path or owner is wrong. Check ls -lZ /var/spool/postfix/private/auth; it should be owned by postfix with mode 0660. |
User doesn't exist: alice@example.com | Dovecot is looking up the full address. Set auth_username_format = %Ln in 10-auth.conf and reload Dovecot. |
lmtp ... Permission denied | Socket ownership in service lmtp does not match the postfix user. Fix 10-master.conf and systemctl restart dovecot. |
SSL_accept() failed / client certificate warnings | Certificate name does not match the hostname clients use, the chain is incomplete (use fullchain.pem), or the key file is unreadable. Verify with the openssl s_client commands above. |
connect to ...:25: Connection timed out | Outbound port 25 is blocked upstream. Relay through your provider or a smart host: postconf -e 'relayhost = [smtp.provider.example]:587' plus SASL client credentials. |
Relay access denied for your own users | The client is sending on port 25 or not authenticating. Use port 587 with authentication. |
| AVC denials | Run sudo ausearch -m AVC -ts recent. Most denials come from mail stored outside the default paths; label custom paths with semanage fcontext and restorecon -Rv. See also Using sed to configure SELinux. |
Differences between RHEL, AlmaLinux, Rocky Linux, Oracle Linux and CentOS Stream
- RHEL: register the system (
subscription-manager register) to get AppStream. EPEL (needed only for certbot and swaks) additionally requires the CodeReady Builder repository. - AlmaLinux and Rocky Linux: package names and versions match RHEL for the same minor release;
dnf install epel-releaseenables EPEL directly. - Oracle Linux: identical packages from
ol9_appstream/ol10_appstream; EPEL is provided asoracle-epel-release-el9/oracle-epel-release-el10. UEK versus RHCK does not matter for mail services. - CentOS Stream: receives updates ahead of the next RHEL minor, so Postfix and Dovecot builds can be newer than on RHEL. The configuration in this guide is the same.
- EL9 to EL10: Berkeley DB is gone (use lmdb:), Sendmail is no longer shipped, and SpamAssassin moved to EPEL. Dovecot stays on the 2.3 series, so the configuration syntax above is unchanged.
Next steps
- Add DKIM signing (OpenDKIM from EPEL) and publish a DMARC record so large providers trust your outbound mail.
- Add spam filtering with Rspamd or SpamAssassin as a milter.
- Move to virtual users stored in LDAP or MariaDB when you outgrow system accounts; Dovecot and Postfix both support these back ends natively.
- For quick application alerts from other servers, point them at this host as a relay; see Send Email Script CentOS for a simple shell example.
- Keep the Postfix and Dovecot Cheat Sheet for RHEL, AlmaLinux and Rocky Linux handy for day-to-day queue, log and doveadm commands.

1 Comment