How to Build a Secure Self‑Hosted Email Server on CentOS with Postfix, Dovecot, MySQL and Roundcube
This step‑by‑step guide shows how to set up a fully functional, self‑hosted mail system on CentOS 7 using Postfix, Dovecot, MariaDB, OpenDKIM and Roundcube Webmail, covering software installation, database schema, service configuration, DNS records, TLS/SSL, firewall rules and testing procedures.
1. Introduction
Why build your own mail system? To avoid reliance on third‑party providers, gain full control, and learn server and networking fundamentals.
The tutorial assumes a fresh CentOS 7.9 minimal install with SELinux and firewall disabled.
2. Required Software & Environment
OS: CentOS 7.9
Postfix 2.10.1, Dovecot 2.2.10, MariaDB 5.5.52, OpenDKIM 2.11.0, Nginx 1.10.2, PHP 5.4.16, Roundcube 1.3.0
Domain: example.com Public IP: 1.1.1.1 Sub‑domain mail.example.com with a Let’s Encrypt certificate
3. Architecture
The following two diagrams illustrate the mail flow (SMTP, LMTP, Dovecot, MySQL, OpenDKIM, Nginx/Roundcube).
Note: SMTP is the sending module, SMTPD is the receiving module.
4. Install Packages
yum -y update && \
yum -y install epel-release && \
yum -y update && \
yum -y install dovecot dovecot-mysql mariadb-server nginx opendkim php-fpm php-mbstring php-mysql php-xml postfix pypolicyd-spf tar wgetIf a key warning appears, answer y and press Enter.
5. Configure MariaDB
5.1 Initialize and Secure MariaDB
systemctl start mariadb mysql_secure_installationFollow the prompts to set a root password and answer “yes” to all security questions.
5.2 Create Mail System Database and Tables
CREATE USER 'mail_sys'@'localhost' IDENTIFIED BY 'mail_sys';
CREATE DATABASE mail_sys;
GRANT SELECT ON mail_sys.* TO 'mail_sys'@'localhost' IDENTIFIED BY 'mail_sys';
FLUSH PRIVILEGES;
USE mail_sys;
CREATE TABLE domains (id int(20) NOT NULL auto_increment, name varchar(100) NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE users (id int(20) NOT NULL auto_increment, domain_id int(20) NOT NULL, password varchar(200) NOT NULL, email varchar(200) NOT NULL, PRIMARY KEY (id), UNIQUE KEY email (email), FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE aliases (id int(20) NOT NULL auto_increment, domain_id int(20) NOT NULL, source varchar(200) NOT NULL, destination varchar(200) NOT NULL, PRIMARY KEY (id), FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8;Insert example rows for domain example.com, users [email protected], [email protected], [email protected], and corresponding aliases.
5.4 Verify Tables
SELECT * FROM mail_sys.domains;
SELECT * FROM mail_sys.users;
SELECT * FROM mail_sys.aliases;Exit MySQL with Ctrl+D.
6. Create Dedicated Mail System User and Group
groupadd -g 2000 mail_sys
useradd -g mail_sys -u 2000 mail_sys -d /var/spool/mail -s /sbin/nologin
chown -R mail_sys:mail_sys /var/spool/mail7. Configure Postfix
7.1 Backup and Edit main.cf
cp -r /etc/postfix /etc/postfix.bak mydomain = example.com
myhostname = mail.example.com
mydestination = localhost
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mynetworks = 127.0.0.0/8
mailbox_size_limit = 0
recipient_delimiter = +
inet_protocols = all
inet_interfaces = all
smtp_address_preference = ipv4
smtpd_banner = ESMTP
biff = no
append_dot_mydomain = no
readme_directory = no
virtual_transport = lmtp:unix:private/dovecot-lmtp
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
virtual_mailbox_domains = mysql:/etc/postfix/mysql_mailbox_domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql_mailbox_maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql_alias_maps.cf
smtpd_sender_login_maps = mysql:/etc/postfix/mysql_mailbox_maps.cf, mysql:/etc/postfix/mysql_alias_maps.cf
disable_vrfy_command = yes
strict_rfc821_envelopes = yes
smtpd_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain, reject_sender_login_mismatch
smtpd_recipient_restrictions = reject_non_fqdn_recipient, reject_unknown_recipient_domain, permit_sasl_authenticated, reject_unauth_destination, check_policy_service unix:private/policyd-spf
virtual_uid_maps = static:2000
virtual_gid_maps = static:2000
message_size_limit = 102400000
smtpd_tls_security_level = may
smtp_tls_security_level = may
smtpd_tls_cert_file=/etc/pki/tls/certs/cert.pem
smtpd_tls_key_file=/etc/pki/tls/private/key.pem
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_tls_protocols = TLSv1.2 TLSv1.1 !TLSv1 !SSLv2 !SSLv3
smtp_tls_protocols = TLSv1.2 TLSv1.1 !TLSv1 !SSLv2 !SSLv3
smtp_tls_ciphers = high
smtpd_tls_ciphers = high
smtpd_tls_mandatory_protocols = TLSv1.2 TLSv1.1 !TLSv1 !SSLv2 !SSLv3
smtp_tls_mandatory_protocols = TLSv1.2 TLSv1.1 !TLSv1 !SSLv2 !SSLv3
smtp_tls_mandatory_ciphers = high
smtpd_tls_mandatory_ciphers = high
smtpd_tls_mandatory_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
smtpd_tls_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
smtp_tls_mandatory_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
smtp_tls_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
tls_preempt_cipherlist = yes
smtpd_tls_received_header = yes
policyd-spf_time_limit = 36007.2 Edit master.cf
cp -r /etc/postfix/master.cf /etc/postfix/master.cf.bak smtp inet n - n - - smtpd
submission inet n - n - - smtpd
-o smtpd_tls_security_level=encrypt
smtps inet n - n - - smtpd
-o smtpd_tls_wrappermode=yes
... (other service definitions as in the original file) ...
policyd-spf unix - n n - 0 spawn
user=mail_sys argv=/usr/libexec/postfix/policyd-spf7.3 MySQL Integration
# /etc/postfix/mysql_mailbox_domains.cf
user = mail_sys
password = mail_sys
hosts = localhost
dbname = mail_sys
query = SELECT 1 FROM domains WHERE name='%s'
# /etc/postfix/mysql_mailbox_maps.cf
user = mail_sys
password = mail_sys
hosts = localhost
dbname = mail_sys
query = SELECT email FROM users WHERE email='%s'
# /etc/postfix/mysql_alias_maps.cf
user = mail_sys
password = mail_sys
hosts = localhost
dbname = mail_sys
query = SELECT destination FROM aliases WHERE source='%s'7.4 Test Database Lookups
systemctl start postfix
postmap -q example.com mysql:/etc/postfix/mysql_mailbox_domains.cf # should return 1
postmap -q [email protected] mysql:/etc/postfix/mysql_mailbox_maps.cf # should return the address
postmap -q [email protected] mysql:/etc/postfix/mysql_alias_maps.cf # should return the real address
systemctl stop postfix8. Configure Dovecot
cp -r /etc/dovecot /etc/dovecot.bak # /etc/dovecot/dovecot.conf
protocols = imap lmtp
!include conf.d/*.conf
!include_try local.conf # /etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:/var/spool/mail/%d/%n
mail_privileged_group = mail # /etc/dovecot/conf.d/15-mailboxes.conf
namespace inbox {
mailbox Drafts { auto = create; special_use = \Drafts; }
mailbox Trash { auto = create; special_use = \Trash; }
mailbox Sent { auto = create; special_use = \Sent; }
} # /etc/dovecot/conf.d/10-auth.conf
auth_mechanisms = plain login
!include auth-sql.conf.ext # /etc/dovecot/conf.d/auth-sql.conf.ext
passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = static
args = uid=mail_sys gid=mail_sys home=/var/spool/mail/%d/%n
} # /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=localhost dbname=mail_sys user=mail_sys password=mail_sys
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM users WHERE email='%u'; # /etc/dovecot/conf.d/10-ssl.conf
ssl = required
ssl_cert = </etc/pki/tls/certs/cert.pem>
ssl_key = </etc/pki/tls/private/key.pem>
ssl_protocols = TLSv1.2 TLSv1.1 !TLSv1 !SSLv2 !SSLv3
ssl_cipher_list = ALL:!MD5:!DES:!ADH:!RC4:!PSD:!SRP:!3DES:!eNULL:!aNULL # /etc/dovecot/conf.d/10-master.conf
service imap-login {
inet_listener imap { port = 143 }
inet_listener imaps { port = 993; ssl = yes }
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600; user = postfix; group = postfix; }
}
service auth {
unix_listener /var/spool/postfix/private/auth { mode = 0666; user = postfix; group = postfix; }
unix_listener auth-userdb { mode = 0600; user = mail_sys; }
user = dovecot
}
service auth-worker { user = mail_sys; } # /etc/dovecot/conf.d/15-lda.conf
postmaster_address = postmaster@%d
protocol lda { }9. Configure OpenDKIM
# /etc/opendkim.conf
Syslog yes
UMask 002
OversignHeaders From
Socket inet:[email protected]
Domain example.com
KeyFile /etc/opendkim/keys/mail.private
Selector mail
RequireSafeKeys no # Generate DKIM keys
opendkim-genkey -D /etc/opendkim/keys/ -d example.com -s mail && \
chown -R opendkim:opendkim /etc/opendkim/keys/ # Add DKIM signing to Postfix main.cf
milter_protocol = 2
milter_default_action = accept
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = inet:127.0.0.1:889110. DNS Records
A record: @ → 1.1.1.1 MX record: @ → mail.example.com and mail → 1.1.1.1 SPF TXT: v=spf1 mx -all DMARC TXT: v=DMARC1; p=reject DKIM TXT (example): mail._domainkey IN TXT "v=DKIM1; k=rsa; p=MI...AB" Set up reverse DNS (PTR) for the public IP pointing to mail.example.com if possible.
11. Install and Configure Roundcube Webmail
# Download and extract
wget https://github.com/roundcube/roundcubemail/releases/download/1.3.0/roundcubemail-1.3.0-complete.tar.gz
tar -xf roundcubemail-1.3.0-complete.tar.gz
mv roundcubemail-1.3.0 /usr/share/roundcube
chown -R apache:apache /usr/share/roundcube # Nginx virtual host (/etc/nginx/conf.d/mail.conf)
server {
listen 80;
server_name mail.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name mail.example.com;
ssl_certificate "/etc/pki/tls/certs/cert.pem";
ssl_certificate_key "/etc/pki/tls/private/key.pem";
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains";
location / {
root /usr/share/roundcube;
index index.php;
}
location ~ \.php$ {
root /usr/share/roundcube;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
} # PHP settings
echo "date.timezone = Asia/Shanghai" >> /etc/php.ini
mkdir -p /var/lib/php/session && chown apache:apache /var/lib/php/session # MariaDB for Roundcube
mysql -u root -p
CREATE USER 'roundcube'@'localhost' IDENTIFIED BY 'roundcube';
CREATE DATABASE roundcube;
GRANT ALL ON roundcube.* TO 'roundcube'@'localhost' IDENTIFIED BY 'roundcube';
FLUSH PRIVILEGES;
exit # Start services
systemctl start nginx php-fpm
systemctl enable nginx php-fpmOpen https://mail.example.com/installer/?_step=1 in a browser, follow the installer to connect to the roundcube database, then secure the installer:
chmod -R 000 /usr/share/roundcube/installer/12. Testing
Receive test: send an email from Gmail/QQ to the new address and verify delivery.
Send test: use mail‑tester.com to send a test email and check SPF, DKIM and DMARC results.
13. Optional Firewall Rules
iptables -A INPUT -p tcp -m multiport --dports 25,465,587,993 -j ACCEPTPersist the rules according to your distribution’s firewall manager.
14. Summary
While the guide covers a functional mail system, spam filtering was omitted due to reliability issues. The setup does not include a web UI for managing users, which is acceptable for a single‑user environment. With proper DNS, TLS, SPF, DKIM and DMARC records, the server can reliably send and receive mail.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
