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-Results showing dkim=pass
  • spf=pass
  • dmarc=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_restrictions is missing permit_sasl_authenticated. Verify main.cf.
  • The domain is not listed in virtual_mailbox_domains. Run postmap -q example.com mysql:/etc/postfix/mysql-virtual-domains.cf to 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 auth socket 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:993 and 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:

  1. Check the Postfix mail queue: mailq
  2. Read the mail log: sudo journalctl -u postfix -n 100
  3. If mail is accepted but not delivered to the maildir, check the Dovecot LMTP log: sudo journalctl -u dovecot -n 100
  4. Verify the LMTP socket exists: ls -la /var/spool/postfix/private/dovecot-lmtp
  5. If sending to external servers fails, check if your IP is on a blocklist. Use mxtoolbox.com to verify.
  6. 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_milters in 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 -y at 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/vhosts directory. 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.

1 COMMENT

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

LEAVE A REPLY

Please enter your comment!
Please enter your name here