Running your own mail server gives you full control over email delivery, storage, and privacy. This guide walks through building a production mail server on Rocky Linux 10 (or AlmaLinux 10) using Postfix as the MTA, Dovecot for IMAP access, MariaDB for virtual user management, and Roundcube as the webmail frontend. I have run this stack in production for years and it remains one of the most reliable self-hosted mail setups available.
What We Are Building
The finished system will have four main components working together:
- Postfix handles all inbound and outbound SMTP traffic. It accepts mail from the internet, checks recipients against MariaDB, and hands messages to Dovecot for local delivery.
- Dovecot provides IMAP (and optionally POP3) access so users can read mail with any client. It also handles SASL authentication for Postfix, meaning users authenticate once through Dovecot and Postfix trusts that result for outbound relay.
- MariaDB stores virtual domains, mailbox accounts, and aliases. No Linux system accounts are needed for mail users. Adding a new mailbox is an INSERT statement.
- Roundcube is a PHP webmail application served by Nginx. It talks IMAP to Dovecot and SMTP to Postfix, giving users a browser-based mail client without installing anything on their machines.
TLS certificates from Let’s Encrypt secure every connection. OpenDKIM signs outbound messages so receiving servers can verify your mail is legitimate. The whole stack runs on a single server, which is perfectly adequate for small to mid-size organizations handling thousands of messages per day.
Prerequisites
Before touching any packages, get the foundation right. Skipping DNS setup is the number one reason self-hosted mail ends up in spam folders or fails outright.
Server Requirements
- Rocky Linux 10 or AlmaLinux 10 minimal install with root or sudo access
- A static public IP address with a clean reputation (check against common blocklists before committing)
- At least 2 GB RAM and 20 GB disk (more if you expect heavy mailbox usage)
- A registered domain name, for example
example.com - Firewall ports open: 25 (SMTP), 587 (submission), 993 (IMAPS), 80 and 443 (HTTP/HTTPS for webmail and Let’s Encrypt)
DNS Records
Set these up at your DNS provider before proceeding. Propagation can take minutes or hours depending on your provider and TTL settings.
A record: Point mail.example.com to your server’s public IP.
MX record: Set example.com MX to mail.example.com with priority 10.
PTR record (reverse DNS): Ask your hosting provider to set the PTR for your IP to mail.example.com. Many receiving servers reject mail when the PTR does not match the HELO hostname.
SPF record: Add a TXT record on example.com:
v=spf1 mx a:mail.example.com ~all
DMARC record: Add a TXT record on _dmarc.example.com:
v=DMARC1; p=quarantine; rua=mailto:[email protected]; fo=1
We will add the DKIM TXT record later after generating the signing key.
Set the Hostname
The server hostname must match your MX and PTR records. Set it now:
sudo hostnamectl set-hostname mail.example.com
Confirm it took effect:
hostname -f
Also update /etc/hosts so the FQDN resolves locally:
sudo vi /etc/hosts
Add a line like:
203.0.113.10 mail.example.com mail
Install and Configure Postfix
Rocky Linux 10 and AlmaLinux 10 ship Postfix in the base repository. Install it along with the MariaDB map support:
sudo dnf install -y postfix postfix-mysql
Enable and start Postfix so it survives reboots:
sudo systemctl enable --now postfix
Main Configuration: /etc/postfix/main.cf
Back up the stock file first, then replace its contents. The configuration below sets up virtual mailbox delivery through Dovecot LMTP, enables TLS, and wires SASL authentication through Dovecot.
sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.bak
Open the file for editing:
sudo vi /etc/postfix/main.cf
Replace the contents with:
# Basic settings
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
inet_interfaces = all
inet_protocols = ipv4
mydestination = localhost
mynetworks = 127.0.0.0/8
# Virtual mailbox settings
virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailboxes.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-aliases.cf
# TLS settings
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_security_level = may
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
# SASL authentication via Dovecot
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
smtpd_sasl_local_domain = $myhostname
# Recipient restrictions
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
reject_unknown_reverse_client_hostname
# Relay restrictions
smtpd_relay_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
defer_unauth_destination
# Message size limit (25 MB)
message_size_limit = 26214400
mailbox_size_limit = 0
# Milter settings for DKIM (configured later)
milter_protocol = 6
milter_default_action = accept
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
Configure Submission Port (587)
Edit /etc/postfix/master.cf to enable the submission port for authenticated users sending mail:
sudo vi /etc/postfix/master.cf
Uncomment or add the submission block:
submission inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
Create Postfix MySQL Lookup Files
These files tell Postfix how to query MariaDB for domains, mailboxes, and aliases. Create them one at a time.
Virtual domains lookup:
sudo vi /etc/postfix/mysql-virtual-domains.cf
Contents:
user = mailuser
password = StrongPasswordHere
hosts = 127.0.0.1
dbname = mailserver
query = SELECT domain FROM virtual_domains WHERE domain='%s'
Virtual mailboxes lookup:
sudo vi /etc/postfix/mysql-virtual-mailboxes.cf
Contents:
user = mailuser
password = StrongPasswordHere
hosts = 127.0.0.1
dbname = mailserver
query = SELECT CONCAT(virtual_domains.domain, '/', virtual_users.email, '/') FROM virtual_users LEFT JOIN virtual_domains ON virtual_users.domain_id = virtual_domains.id WHERE virtual_users.email='%s'
Virtual aliases lookup:
sudo vi /etc/postfix/mysql-virtual-aliases.cf
Contents:
user = mailuser
password = StrongPasswordHere
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s'
Lock down permissions on these files since they contain database credentials:
sudo chmod 640 /etc/postfix/mysql-virtual-*.cf
sudo chgrp postfix /etc/postfix/mysql-virtual-*.cf
Create the MariaDB Database and Tables
Install MariaDB if it is not already present:
sudo dnf install -y mariadb-server
Enable and start the service:
sudo systemctl enable --now mariadb
Run the initial security hardening:
sudo mysql_secure_installation
Now log in and create the mail database, tables, and a dedicated database user. Replace passwords with your own strong values.
sudo mysql -u root -p
Run the following SQL statements inside the MariaDB prompt:
CREATE DATABASE mailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE mailserver;
CREATE TABLE virtual_domains (
id INT NOT NULL AUTO_INCREMENT,
domain VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (domain)
) ENGINE=InnoDB;
CREATE TABLE virtual_users (
id INT NOT NULL AUTO_INCREMENT,
domain_id INT NOT NULL,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY (email),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE virtual_aliases (
id INT NOT NULL AUTO_INCREMENT,
domain_id INT NOT NULL,
source VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE USER 'mailuser'@'127.0.0.1' IDENTIFIED BY 'StrongPasswordHere';
GRANT SELECT ON mailserver.* TO 'mailuser'@'127.0.0.1';
FLUSH PRIVILEGES;
Add your domain and a test user. Dovecot expects passwords hashed with BLF-CRYPT (bcrypt), which is the strongest scheme it supports:
INSERT INTO virtual_domains (domain) VALUES ('example.com');
INSERT INTO virtual_users (domain_id, email, password)
VALUES (1, '[email protected]', '{BLF-CRYPT}$2y$10$HASH_FROM_DOVEADM');
INSERT INTO virtual_aliases (domain_id, source, destination)
VALUES (1, '[email protected]', '[email protected]');
EXIT;
To generate the password hash, use the doveadm tool (available after Dovecot is installed):
doveadm pw -s BLF-CRYPT -p YourUserPassword
Copy the full output including the {BLF-CRYPT} prefix and use it in the INSERT statement above.
Install and Configure Dovecot
Install Dovecot with the MySQL and LMTP modules:
sudo dnf install -y dovecot dovecot-mysql dovecot-pigeonhole
Enable and start the service:
sudo systemctl enable --now dovecot
Create the vmail User
All virtual mailboxes will be owned by a dedicated system user. This keeps things clean and avoids permission headaches:
sudo groupadd -g 5000 vmail
sudo useradd -g vmail -u 5000 -d /var/mail/vhosts -s /sbin/nologin vmail
sudo mkdir -p /var/mail/vhosts
sudo chown -R vmail:vmail /var/mail/vhosts
Dovecot Main Configuration
Edit /etc/dovecot/dovecot.conf:
sudo vi /etc/dovecot/dovecot.conf
Set the protocols:
protocols = imap lmtp sieve
Mail Location
Edit /etc/dovecot/conf.d/10-mail.conf:
sudo vi /etc/dovecot/conf.d/10-mail.conf
Set the mail location and user/group:
mail_location = maildir:/var/mail/vhosts/%d/%n
mail_uid = vmail
mail_gid = vmail
mail_privileged_group = vmail
first_valid_uid = 5000
last_valid_uid = 5000
Authentication Configuration
Edit /etc/dovecot/conf.d/10-auth.conf. Disable plain text auth except over TLS and enable the SQL backend:
sudo vi /etc/dovecot/conf.d/10-auth.conf
Set these values:
disable_plaintext_auth = yes
auth_mechanisms = plain login
# Comment out the default system auth and enable SQL:
#!include auth-system.conf.ext
!include auth-sql.conf.ext
SQL Authentication File
Edit /etc/dovecot/conf.d/auth-sql.conf.ext:
sudo vi /etc/dovecot/conf.d/auth-sql.conf.ext
Contents:
passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = static
args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n
}
Now create the SQL connection file:
sudo vi /etc/dovecot/dovecot-sql.conf.ext
Contents:
driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailuser password=StrongPasswordHere
default_pass_scheme = BLF-CRYPT
password_query = SELECT email AS user, password FROM virtual_users WHERE email='%u'
TLS Configuration
Edit /etc/dovecot/conf.d/10-ssl.conf:
sudo vi /etc/dovecot/conf.d/10-ssl.conf
Set the following:
ssl = required
ssl_cert = </etc/letsencrypt/live/mail.example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.example.com/privkey.pem
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
LMTP Socket and Auth Socket for Postfix
Edit /etc/dovecot/conf.d/10-master.conf. This file controls the sockets Dovecot exposes. Find or add the following blocks:
sudo vi /etc/dovecot/conf.d/10-master.conf
Configure the LMTP service so Postfix can hand off messages:
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
Configure the auth service so Postfix can use Dovecot for SASL:
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
unix_listener auth-userdb {
mode = 0600
user = vmail
}
user = dovecot
}
service auth-worker {
user = vmail
}
Secure the Dovecot SQL config file:
sudo chown root:root /etc/dovecot/dovecot-sql.conf.ext
sudo chmod 640 /etc/dovecot/dovecot-sql.conf.ext
TLS Certificates with Let’s Encrypt
Both Postfix and Dovecot need valid TLS certificates. Let’s Encrypt provides them for free. Install certbot:
sudo dnf install -y certbot
Obtain the certificate using standalone mode. Make sure port 80 is open and nothing else is listening on it yet:
sudo certbot certonly --standalone -d mail.example.com
Once the certificate is issued, both Postfix and Dovecot will pick it up from /etc/letsencrypt/live/mail.example.com/ as configured earlier.
Set up automatic renewal with a post-hook that reloads all three services:
sudo vi /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh
Contents:
#!/bin/bash
systemctl reload postfix
systemctl reload dovecot
systemctl reload nginx
Make it executable:
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh
Test the renewal process to confirm everything works:
sudo certbot renew --dry-run
Now restart Postfix and Dovecot so they load the new certificates:
sudo systemctl restart postfix dovecot
Install and Configure Roundcube with Nginx
Roundcube gives your users a webmail interface without requiring a desktop client. Install Nginx, PHP, and the required PHP modules:
sudo dnf install -y nginx php-fpm php-mysqlnd php-xml php-mbstring php-intl php-zip php-gd php-json php-curl php-ldap php-pear-Net-SMTP php-pear-Net-Socket
Enable and start Nginx and PHP-FPM:
sudo systemctl enable --now nginx php-fpm
Download Roundcube
Grab the latest stable release from the Roundcube website. At the time of writing, 1.6.x is the current stable branch. Adjust the version number as needed:
cd /tmp
curl -LO https://github.com/roundcube/roundcubemail/releases/download/1.6.9/roundcubemail-1.6.9-complete.tar.gz
tar xzf roundcubemail-1.6.9-complete.tar.gz
sudo mv roundcubemail-1.6.9 /var/www/roundcube
sudo chown -R nginx:nginx /var/www/roundcube
Create the Roundcube Database
Log in to MariaDB and create a separate database for Roundcube:
sudo mysql -u root -p
Run these SQL statements:
CREATE DATABASE roundcubemail CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'roundcube'@'localhost' IDENTIFIED BY 'RoundcubeDBPassword';
GRANT ALL PRIVILEGES ON roundcubemail.* TO 'roundcube'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Import the Roundcube schema:
sudo mysql -u root -p roundcubemail < /var/www/roundcube/SQL/mysql.initial.sql
Configure Roundcube
Copy the sample config:
sudo cp /var/www/roundcube/config/config.inc.php.sample /var/www/roundcube/config/config.inc.php
sudo vi /var/www/roundcube/config/config.inc.php
Set the key values:
$config['db_dsnw'] = 'mysql://roundcube:RoundcubeDBPassword@localhost/roundcubemail';
$config['imap_host'] = 'ssl://mail.example.com:993';
$config['smtp_host'] = 'tls://mail.example.com:587';
$config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p';
$config['support_url'] = '';
$config['product_name'] = 'Webmail';
$config['des_key'] = 'Generate_A_Random_24_Char_Key!';
$config['plugins'] = ['archive', 'zipdownload', 'managesieve'];
$config['language'] = 'en_US';
$config['skin'] = 'elastic';
Generate a random key for des_key with:
openssl rand -base64 24
Nginx Virtual Host for Roundcube
Create the Nginx server block:
sudo vi /etc/nginx/conf.d/roundcube.conf
Contents:
server {
listen 443 ssl http2;
server_name mail.example.com;
ssl_certificate /etc/letsencrypt/live/mail.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
root /var/www/roundcube;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm/www.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\. {
deny all;
}
location ~* ^/(config|temp|logs)/ {
deny all;
}
}
server {
listen 80;
server_name mail.example.com;
return 301 https://$host$request_uri;
}
Test the Nginx configuration and reload:
sudo nginx -t && sudo systemctl reload nginx
Update PHP-FPM to run as the nginx user. Edit /etc/php-fpm.d/www.conf and set:
user = nginx
group = nginx
Restart PHP-FPM:
sudo systemctl restart php-fpm
After restarting, remove the installer directory for security:
sudo rm -rf /var/www/roundcube/installer
DKIM Signing with OpenDKIM
DKIM adds a cryptographic signature to every outbound message, letting receivers verify the message actually came from your server. This is not optional if you care about deliverability.
Install OpenDKIM:
sudo dnf install -y opendkim opendkim-tools
Generate DKIM Keys
Create the key directory and generate a 2048-bit key pair:
sudo mkdir -p /etc/opendkim/keys/example.com
sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s default -v
sudo chown -R opendkim:opendkim /etc/opendkim/keys
View the DNS record you need to add:
sudo cat /etc/opendkim/keys/example.com/default.txt
Copy that output and add it as a TXT record for default._domainkey.example.com at your DNS provider. The record will contain the public key in a format like v=DKIM1; k=rsa; p=MIIBIjANBg....
Configure OpenDKIM
Edit the main config file:
sudo vi /etc/opendkim.conf
Set or update these values:
Mode sv
Socket inet:8891@localhost
Canonicalization relaxed/simple
Domain example.com
Selector default
KeyFile /etc/opendkim/keys/example.com/default.private
SignatureAlgorithm rsa-sha256
AutoRestart yes
AutoRestartRate 10/1M
SyslogSuccess yes
LogWhy yes
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
Create the supporting files. First, TrustedHosts:
sudo vi /etc/opendkim/TrustedHosts
Contents:
127.0.0.1
::1
localhost
mail.example.com
example.com
KeyTable:
sudo vi /etc/opendkim/KeyTable
Contents:
default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private
SigningTable:
sudo vi /etc/opendkim/SigningTable
Contents:
*@example.com default._domainkey.example.com
Enable and start OpenDKIM:
sudo systemctl enable --now opendkim
Restart Postfix so it connects to the OpenDKIM milter:
sudo systemctl restart postfix
Firewall Configuration
Open the required ports through firewalld:
sudo firewall-cmd --permanent --add-service={smtp,smtps,imap,imaps,http,https}
sudo firewall-cmd --permanent --add-port=587/tcp
sudo firewall-cmd --reload
Test Sending and Receiving
With everything configured and running, it is time to verify the whole pipeline works. Install swaks, a command-line SMTP testing tool that every mail admin should have:
sudo dnf install -y swaks
Test Authenticated Sending
Send a test message through the submission port with authentication:
swaks --to [email protected] \
--from [email protected] \
--server mail.example.com \
--port 587 \
--auth LOGIN \
--auth-user [email protected] \
--auth-password YourUserPassword \
--tls
Watch the output. You should see a 250 OK response at the end indicating the message was accepted for delivery.
Test Inbound Delivery
Send a message from an external account (Gmail, for example) to [email protected]. Then check the maildir on the server:
ls -la /var/mail/vhosts/example.com/[email protected]/new/
You should see a new file for each delivered message.
Test IMAP Access
Verify Dovecot IMAP is working with openssl:
openssl s_client -connect mail.example.com:993
Once connected, authenticate with:
a1 LOGIN [email protected] YourUserPassword
a2 LIST "" "*"
a3 LOGOUT
You should see the mailbox listing. At this point you can also log in to Roundcube at https://mail.example.com using the same credentials.
Verify DKIM, SPF, and DMARC
Send a test email to a Gmail address and view the original message headers. Look for:
Authentication-Resultsshowingdkim=passspf=passdmarc=pass
You can also use external tools like mail-tester.com to get a full deliverability report.
Anti-Spam: SpamAssassin or Rspamd
A mail server without spam filtering will drown in junk within hours. You have two solid options on Rocky Linux 10.
Option A: SpamAssassin
SpamAssassin has been the standard for years and works well for moderate traffic:
sudo dnf install -y spamassassin
Enable and start the service:
sudo systemctl enable --now spamassassin
Update the spam rules:
sudo sa-update
To integrate with Postfix, add a content filter in /etc/postfix/master.cf. Add this line at the bottom:
spamassassin unix - n n - - pipe
flags=DRhu user=vmail argv=/usr/bin/spamc -f -e /usr/libexec/dovecot/deliver -d ${user}@${domain}
Then in /etc/postfix/main.cf, add:
content_filter = spamassassin
Option B: Rspamd
Rspamd is the more modern choice. It is faster, integrates DKIM/SPF/DMARC checking natively, and has a web UI for monitoring. If you are building a new server, I would recommend Rspamd over SpamAssassin.
Install Rspamd (you may need to add the Rspamd repository first):
sudo dnf install -y rspamd redis
Enable and start both services:
sudo systemctl enable --now rspamd redis
Rspamd uses a milter interface, same as OpenDKIM. To use Rspamd instead of OpenDKIM for DKIM signing (recommended since Rspamd handles it natively), update the milter settings in /etc/postfix/main.cf:
smtpd_milters = inet:localhost:11332
non_smtpd_milters = inet:localhost:11332
Place the DKIM key in /var/lib/rspamd/dkim/ and configure the DKIM signing module at /etc/rspamd/local.d/dkim_signing.conf:
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector = "default";
Restart Rspamd and Postfix after making changes:
sudo systemctl restart rspamd postfix
SELinux Considerations
Rocky Linux 10 ships with SELinux in enforcing mode. If you hit unexpected "permission denied" errors, check the audit log before disabling SELinux. Usually a few boolean toggles are all you need:
sudo setsebool -P httpd_can_network_connect on
sudo setsebool -P httpd_can_sendmail on
If Postfix cannot connect to the MariaDB socket or Dovecot LMTP socket, check for AVC denials:
sudo ausearch -m avc -ts recent
Then generate and apply a custom policy module if needed:
sudo ausearch -m avc -ts recent | audit2allow -M mailserver_custom
sudo semodule -i mailserver_custom.pp
Troubleshooting
After years of running production mail servers, these are the issues I see most often.
Relay Access Denied (454 or 550)
This means Postfix is refusing to relay mail. The most common causes:
- The client is not authenticating. Check that the mail client is configured to use port 587 with STARTTLS and authentication enabled.
smtpd_relay_restrictionsis missingpermit_sasl_authenticated. Verify main.cf.- The domain is not listed in
virtual_mailbox_domains. Runpostmap -q example.com mysql:/etc/postfix/mysql-virtual-domains.cfto test the lookup.
Authentication Failed
Check the Dovecot logs first:
sudo journalctl -u dovecot -n 50
Common causes:
- Wrong password hash scheme. Make sure the password in MariaDB uses the same scheme configured in
dovecot-sql.conf.ext(BLF-CRYPT). - The
authsocket is not accessible to Postfix. Check permissions on/var/spool/postfix/private/auth. - SQL query returning no results. Test manually:
doveadm auth test [email protected] password.
TLS Errors
If mail clients show certificate warnings or connections fail:
- Verify the certificate path. Run
openssl s_client -connect mail.example.com:993and check the certificate subject matches your hostname. - Make sure Postfix and Dovecot are both pointing to the fullchain.pem, not just the cert.pem. The chain matters.
- After renewing Let's Encrypt certificates, services must be reloaded. The deploy hook script above handles this automatically.
Mail Not Arriving
Work through this systematically:
- Check the Postfix mail queue:
mailq - Read the mail log:
sudo journalctl -u postfix -n 100 - If mail is accepted but not delivered to the maildir, check the Dovecot LMTP log:
sudo journalctl -u dovecot -n 100 - Verify the LMTP socket exists:
ls -la /var/spool/postfix/private/dovecot-lmtp - If sending to external servers fails, check if your IP is on a blocklist. Use mxtoolbox.com to verify.
- Check that DNS is resolving correctly from the server:
dig MX example.com
DKIM Not Signing
If outbound messages lack DKIM signatures:
- Confirm OpenDKIM is running:
sudo systemctl status opendkim - Verify Postfix is connecting to the milter: check
smtpd_miltersin main.cf. - Look at the OpenDKIM log:
sudo journalctl -u opendkim -n 50 - Test the DKIM DNS record:
dig TXT default._domainkey.example.com
Ongoing Maintenance
A mail server is not something you set up and walk away from. Keep these in your routine:
- Monitor disk usage. Maildir storage grows fast when users do not clean up. Set up a cron job or use Dovecot quota plugins to enforce limits.
- Keep the system updated:
sudo dnf update -yat least weekly. - Review mail logs regularly for unusual patterns: brute-force login attempts, unexpected relay attempts, or delivery failures spiking.
- Test your spam score periodically by sending to mail-tester.com.
- Back up the MariaDB databases and the
/var/mail/vhostsdirectory. A mail server without backups is a disaster waiting to happen. - Monitor certificate expiry. Let's Encrypt certificates last 90 days and certbot handles renewal, but verify the cron job is actually running.
Summary
You now have a fully functional mail server on Rocky Linux 10 (or AlmaLinux 10) with Postfix handling SMTP, Dovecot providing IMAP with MariaDB-backed virtual users, Roundcube for webmail, TLS everywhere via Let's Encrypt, and DKIM signing for outbound messages. The same setup works identically on AlmaLinux 10 since both distributions share the same package base.
This stack has served me well on production servers handling anywhere from a few dozen to several thousand mailboxes. The key to keeping it running smoothly is monitoring, timely updates, and good DNS hygiene. If you follow the DNS setup and troubleshooting steps outlined here, your messages should land in inboxes rather than spam folders.


























































Hello Klinsmann, Iḿ trying to follow your guide but I have some troubles, mainly in the database use of passwords. You use as example the ENCRYPT(‘Password’, CONCAT(‘$6$’, SUBSTRING(SHA(RAND()), -16))) data field, suppously “Password” need to be changed for whatever the user password needs to be set, but also in dovecot uses the next login method
driver = mysql
connect = “host=127.0.0.1 dbname=postfix_accounts user=postfix_admin password=StrongPassw0rd”
default_pass_scheme = SHA512-CRYPT
password_query = SELECT Email as User, password FROM accounts_table WHERE Email=’%u’;
, thats uses SHA512-CRYPT, and thats my dude, how this correlate?, in my test I have troubles with authentification.
Thanks in advance.