Build a Self‑Hosted Email Server on CentOS 7: Step‑by‑Step Guide
Learn how to set up a complete email system on CentOS 7 using Postfix, Dovecot, MariaDB, Nginx, and Roundcube, covering software installation, configuration, database setup, SSL/TLS, DKIM, SPF, DMARC, and testing, with detailed commands and troubleshooting tips.
Introduction
This guide shows how to build a fully functional self‑hosted email system on a CentOS 7 server. It covers everything from installing required software to configuring Postfix, Dovecot, MariaDB, OpenDKIM, Nginx, and Roundcube, as well as setting up DNS records and testing the setup.
Prerequisites
CentOS 7.9 minimal installation (SELinux and firewall disabled)
Domain name (e.g., example.com) and public IP (e.g., 1.1.1.1)
Root access to the server
Install Required 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, press y and hit Enter.
Configure MariaDB (MySQL)
Initialize and Secure MariaDB
systemctl start mariadb
mysql_secure_installationFollow the prompts to set a root password and answer y to all questions.
Create Database and Users
CREATE DATABASE mail_sys;
CREATE USER 'mail_sys'@'localhost' IDENTIFIED BY 'mail_sys';
GRANT SELECT ON mail_sys.* TO 'mail_sys'@'localhost' IDENTIFIED BY 'mail_sys';
FLUSH PRIVILEGES;Create Tables
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;Configure Postfix
Backup Original Configuration
cp -r /etc/postfix /etc/postfix.bakmain.cf (core parameters)
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_exclude_ciphers = MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL
tls_preempt_cipherlist = yes
smtpd_tls_received_header = yes
policyd-spf_time_limit = 3600master.cf (service definitions)
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 omitted for brevity) ...Configure Dovecot
Backup Original Configuration
cp -r /etc/dovecot /etc/dovecot.bakdovecot.conf
protocols = imap lmtp
!include conf.d/*.conf
!include_try local.conf10-mail.conf
namespace inbox {
inbox = yes
}
first_valid_uid = 1000
mbox_write_locks = fcntl
mail_location = maildir:/var/spool/mail/%d/%n
mail_privileged_group = mail15-mailboxes.conf (special folders)
namespace inbox {
mailbox Drafts {
auto = create
special_use = \Drafts
}
mailbox Trash {
auto = create
special_use = \Trash
}
mailbox Sent {
auto = create
special_use = \Sent
}
}10-auth.conf
auth_mechanisms = plain login
!include auth-sql.conf.extauth-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
}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';10-ssl.conf
ssl = required
ssl_cert =Replace /etc/pki/tls/certs/cert.pem and /etc/pki/tls/private/key.pem with your actual certificate files.
10-master.conf (IMAP/LMTP listeners)
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
}Configure OpenDKIM
opendkim.conf
Syslog yes
UMask 002
OversignHeaders From
Socket inet:[email protected]
Domain example.com
KeyFile /etc/opendkim/keys/mail.private
Selector mail
RequireSafeKeys noGenerate Keys
opendkim-genkey -D /etc/opendkim/keys/ -d example.com -s mail && \
chown -R opendkim:opendkim /etc/opendkim/keys/Add DKIM Milter to Postfix
milter_protocol = 2
milter_default_action = accept
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = inet:127.0.0.1:8891Configure Nginx and Roundcube (Webmail)
Nginx Virtual Host
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/sessionRoundcube Database
CREATE USER 'roundcube'@'localhost' IDENTIFIED BY 'roundcube';
CREATE DATABASE roundcube;
GRANT ALL ON roundcube.* TO 'roundcube'@'localhost' IDENTIFIED BY 'roundcube';
FLUSH PRIVILEGES;Complete the web installer at https://mail.example.com/installer/?_step=1 and then lock the installer directory:
chmod -R 000 /usr/share/roundcube/installer/DNS Records
A record : @ → 1.1.1.1 MX record : @ → mail.example.com SPF record : @ TXT "v=spf1 mx -all" DMARC record : _dmarc TXT "v=DMARC1; p=reject" DKIM record : publish the p=… value from /etc/opendkim/keys/mail.txt as mail._domainkey TXT "v=DKIM1; k=rsa; p=..." PTR record (reverse DNS) should point the server IP to mail.example.com if possible.
Testing
Receive Test
Send an email from an external account (e.g., Gmail or QQ) to [email protected]. Verify it appears in Roundcube.
Send Test
Use mail‑tester.com to send a test email and check that SPF, DKIM, and DMARC pass.
Optional Firewall Configuration
iptables -A INPUT -p tcp -m multiport --dports 25,465,587,993 -j ACCEPTSave the firewall rules according to your distribution's method.
Conclusion
This tutorial provides a complete, reproducible method to deploy a secure, self‑hosted email service on CentOS 7. By following the steps, you obtain a functional SMTP/IMAP server with webmail access, proper DNS authentication, and basic firewall hardening.
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.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
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.
