Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Postfix Dovecot Ubuntu
GUIDEInstall Guides

How to Install Postfix + Dovecot on Ubuntu 24.04 VPS: Manual Self-Hosted Mail

27 min read

How to Install Postfix + Dovecot on Ubuntu 24.04 VPS: Manual Self-Hosted Mail

Running your own mail server teaches you more about the internet than almost any other single project. SMTP, IMAP, TLS, DNS, DKIM, SPF, DMARC, reverse DNS, greylisting, content filtering -- they all intersect the moment you try to deliver a message from your VPS to a Gmail inbox. This guide walks you through a manual installation of Postfix (SMTP) and Dovecot (IMAP/POP3) on an Ubuntu 24.04 VPS, backed by MariaDB for virtual users, OpenDKIM for message signing, Rspamd for spam filtering, and Let's Encrypt for TLS.

Want a managed mail stack instead? If you'd rather skip the manual build, Mailcow and Mailu both run well on our CloudCore Professional VPS. This guide is for operators who want to understand every moving part.

Table of Contents

  • Why Build a Mail Server Manually?
  • Prerequisites
  • Step 1: Set the Hostname and FQDN
  • Step 2: Configure DNS Records (MX, SPF, rDNS, DMARC)
  • Step 3: Install the Mail Server Packages
  • Step 4: Create the MariaDB Schema for Virtual Users
  • Step 5: Obtain a Let's Encrypt TLS Certificate
  • Step 6: Configure Postfix (main.cf and master.cf)
  • Step 7: Wire Up the Postfix to MySQL Lookup Files
  • Step 8: Configure Dovecot (Mail, Auth, Master, SSL)
  • Step 9: Install and Configure OpenDKIM
  • Step 10: Install Rspamd and Hook It Into Postfix
  • Step 11: Test Send and Receive
  • Troubleshooting
  • FAQ
  • Next Steps
  • Why Build a Mail Server Manually?

    You can install Mailcow or Mailu in twenty minutes and get a working mail server with a web UI. That's the right choice for production if you don't enjoy debugging SMTP. But manual installation is worth doing at least once because it forces you to understand every component you'll later rely on:

    • Deep understanding of the SMTP/IMAP stack -- You learn exactly how a message moves from a user's mail client through Postfix's smtpd, into Dovecot's LMTP delivery, onto disk as a Maildir file, and back out through Dovecot's imap-login when the user reads it. When something breaks in a managed stack, you know where to look.
    • Complete control over policy -- Custom rate limits, custom header rewrites, custom reject messages, custom routing per domain. Managed stacks expose a subset of Postfix's knobs through a UI; manual setup gives you all of them.
    • Smaller attack surface -- A Mailcow install runs ~15 Docker containers. A manual stack is Postfix, Dovecot, OpenDKIM, Rspamd, MariaDB, and Nginx -- six processes you can actually reason about.
    • No hidden cost in RAM -- Docker-based mail stacks frequently need 4-6 GB of RAM. A manual Postfix/Dovecot setup runs comfortably in under 1 GB, leaving room for other workloads on the same VPS.
    • Full customization at every layer -- Want to route outbound mail through a smart host for reputation? Want to sign with multiple DKIM selectors per domain? Want to terminate TLS at HAProxy? All straightforward with a manual build.
    • Portability -- Once you know the files, you can rebuild the server on any Linux distro in under an hour. Managed stacks tie you to their version lifecycle.

    Manual vs. Managed Mail Stack

    FactorManual (this guide)Mailcow / Mailu
    Setup time45-60 minutes15-20 minutes
    RAM footprint~800 MB idle3-6 GB idle
    Webmail UINot included (add Roundcube)Included
    Admin UIEdit config filesWeb-based
    Upgrade pathapt upgradeImage tag bump
    Customization ceilingUnlimitedLimited to exposed options
    Best forLearning + custom policyFast production deploys
    If you are managing five or more mail domains for end users, a managed stack probably pays for itself. For personal use, a handful of domains, or a learning exercise, manual wins.

    Prerequisites

    Before you begin, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server
    • A registered domain you control the DNS for (we'll use example.com throughout)
    • A fully qualified hostname for the mail server (we'll use mail.example.com)
    • Reverse DNS (PTR) set on the VPS IP to match mail.example.com -- this is done through your VPS provider's control panel, not DNS
    • Ports 25, 465, 587, 993, and 995 open on the firewall and not blocked by your provider (some providers block port 25 outbound by default -- open a ticket to request unblocking)
    • At least 2 GB of RAM and 20 GB of disk space
    Recommended Plan: CloudCore Professional
    >
    For a single-domain mail server with Rspamd, ClamAV-free Rspamd statistical filtering, and moderate volume (up to ~50,000 messages/day), we recommend the CloudCore Professional plan:
    >
    - 6 vCPU cores
    - 12 GB RAM
    - 100 GB NVMe SSD
    - Unmetered bandwidth
    - Clean IP reputation with rDNS configurable from the panel
    >
    Mail servers are disk-bound on the Maildir side, so NVMe storage makes a measurable difference when users have large mailboxes or slow IMAP clients doing full sync.

    Connect to your server via SSH:

    bash
    ssh root@your-server-ip

    Update the system before you start:

    bash
    sudo apt update && sudo apt upgrade -y

    If the kernel was upgraded, reboot:

    bash
    sudo reboot

    Step 1: Set the Hostname and FQDN

    Every mail server needs a fully qualified hostname that matches its rDNS and its A record. Receiving mail servers (especially Gmail and Outlook) will reject mail from a host whose HELO name doesn't resolve correctly.

    Set the hostname:

    bash
    sudo hostnamectl set-hostname mail.example.com

    Edit /etc/hosts so the FQDN resolves locally:

    bash
    sudo nano /etc/hosts

    Add or modify the line for 127.0.1.1:

    text
    127.0.0.1       localhost
    127.0.1.1       mail.example.com mail

    Verify the hostname is set correctly:

    bash
    hostname -f

    Expected output:

    text
    mail.example.com

    Check that the system's A record resolves back to your server's public IP:

    bash
    dig +short mail.example.com A

    This should return your VPS IP. If it doesn't, fix your DNS before proceeding.

    Step 2: Configure DNS Records (MX, SPF, rDNS, DMARC)

    Mail deliverability is 80 percent DNS. Get these records right and Gmail will accept your mail; get them wrong and every message hits spam or bounces.

    Create the following records at your DNS provider (Cloudflare, Route 53, your registrar's panel, etc.):

    A record for the mail host:

    TypeNameValue
    Amail.example.comYOUR_VPS_IP
    MX record so the world knows which host handles mail for your domain:
    TypeNameValuePriority
    MXexample.commail.example.com.10
    SPF record as a TXT on the root domain, authorizing your mail host's IP to send on behalf of your domain:
    TypeNameValue
    TXTexample.com"v=spf1 mx ~all"
    DMARC record so receiving servers know what to do with failed auth:
    TypeNameValue
    TXT_dmarc.example.com"v=DMARC1; p=none; rua=mailto:[email protected]; pct=100; adkim=s; aspf=s"
    Start with p=none and move to p=quarantine then p=reject once you've reviewed aggregate reports for a week or two.

    DKIM placeholder -- we'll generate the actual value in Step 9. Leave room for a TXT at mail._domainkey.example.com.

    Reverse DNS (PTR) -- go to your VPS provider's control panel and set the PTR record for your server's IP to mail.example.com. This is critical. Many receiving servers reject mail when the sender's IP PTR doesn't match its HELO hostname.

    Verify everything once DNS has propagated:

    bash
    # A record
    dig +short mail.example.com A

    MX record

    dig +short example.com MX

    SPF

    dig +short example.com TXT

    DMARC

    dig +short _dmarc.example.com TXT

    rDNS (replace with your IP)

    dig +short -x 203.0.113.10

    The rDNS lookup should return mail.example.com.. If it doesn't, open a ticket with your VPS provider or set it in their panel before continuing.

    For a deeper treatment of each record, see our DNS configuration guide.

    Step 3: Install the Mail Server Packages

    Install everything in one pass. Postfix's interactive prompt will appear during install -- when it asks for a configuration type, pick "Internet Site" and set the mail name to your FQDN (mail.example.com).

    bash
    sudo DEBIAN_FRONTEND=noninteractive apt install -y \
      postfix postfix-mysql \
      dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
      mariadb-server \
      opendkim opendkim-tools \
      rspamd \
      certbot

    Package roles:

    • postfix + postfix-mysql -- SMTP server plus the MySQL map lookup plugin.
    • dovecot-core / imapd / pop3d / lmtpd / mysql -- IMAP and POP3 for mail clients, LMTP for local delivery from Postfix, and MySQL support for the user database.
    • mariadb-server -- relational store for virtual domains, users, and aliases.
    • opendkim + opendkim-tools -- milter that signs outbound mail with DKIM and verifies inbound.
    • rspamd -- spam filter with built-in statistical, Bayesian, and network-based rules.
    • certbot -- Let's Encrypt client for TLS certificates.
    Harden MariaDB right after install:

    bash
    sudo mysql_secure_installation

    Answer: set a root password, remove anonymous users, disallow remote root login, remove the test database, reload privilege tables. See our MariaDB hardening guide for a deeper walkthrough.

    Enable and start MariaDB if it isn't already:

    bash
    sudo systemctl enable --now mariadb

    Step 4: Create the MariaDB Schema for Virtual Users

    Postfix and Dovecot both need to look up domains, users, and aliases. We'll store them in a small MariaDB schema that both services query read-only.

    Connect to MariaDB:

    bash
    sudo mariadb

    Create the database, user, and tables:

    sql
    CREATE DATABASE mailserver CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

    CREATE USER 'mailuser'@'127.0.0.1' IDENTIFIED BY 'CHANGE_ME_STRONG_PASSWORD'; GRANT SELECT ON mailserver.* TO 'mailuser'@'127.0.0.1'; FLUSH PRIVILEGES;

    USE mailserver;

    CREATE TABLE virtual_domains ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(191) NOT NULL UNIQUE, PRIMARY KEY (id) ) ENGINE=InnoDB;

    CREATE TABLE virtual_users ( id INT NOT NULL AUTO_INCREMENT, domain_id INT NOT NULL, email VARCHAR(191) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, PRIMARY KEY (id), 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(191) NOT NULL, destination VARCHAR(191) NOT NULL, PRIMARY KEY (id), FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE ) ENGINE=InnoDB;

    Seed one domain, one user, and one alias. Generate a SHA256-CRYPT hash for the password using Dovecot's helper:

    bash
    doveadm pw -s SHA256-CRYPT -p 'YourMailboxPassword'

    Expected output (yours will differ):

    text
    {SHA256-CRYPT}$5$rounds=5000$abc123...$xyz789...

    Copy that full string (including the {SHA256-CRYPT} prefix) and insert it:

    sql
    INSERT INTO virtual_domains (name) VALUES ('example.com');

    INSERT INTO virtual_users (domain_id, email, password) VALUES (1, '[email protected]', '{SHA256-CRYPT}$5$rounds=5000$abc123...$xyz789...');

    INSERT INTO virtual_aliases (domain_id, source, destination) VALUES (1, '[email protected]', '[email protected]');

    EXIT;

    The source is the address that receives mail; the destination is where it gets forwarded. Aliases are checked before user mailboxes, so [email protected] will resolve to [email protected] at delivery time.

    Step 5: Obtain a Let's Encrypt TLS Certificate

    Postfix and Dovecot both need a TLS certificate for encrypted client connections (submission on 587, SMTPS on 465, IMAPS on 993). Let's Encrypt issues free, trusted certificates that renew automatically.

    Stop any service binding port 80 temporarily (if Nginx is installed):

    bash
    sudo systemctl stop nginx 2>/dev/null || true

    Request the certificate using the standalone challenge:

    bash
    sudo certbot certonly --standalone \
      -d mail.example.com \
      --agree-tos -m [email protected] --no-eff-email

    Expected output:

    text
    Successfully received certificate.
    Certificate is saved at: /etc/letsencrypt/live/mail.example.com/fullchain.pem
    Key is saved at:         /etc/letsencrypt/live/mail.example.com/privkey.pem

    Certbot installs a systemd timer (certbot.timer) that renews certificates automatically. After each renewal, Postfix and Dovecot need to be restarted to load the new certificate. Create a renewal hook:

    bash
    sudo tee /etc/letsencrypt/renewal-hooks/deploy/mail-reload.sh > /dev/null <<'EOF'
    #!/bin/bash
    systemctl reload postfix
    systemctl reload dovecot
    EOF

    sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail-reload.sh

    For more on certificate management, see our Let's Encrypt setup guide.

    Step 6: Configure Postfix (main.cf and master.cf)

    This is the heart of the setup. Postfix's configuration lives in two files: /etc/postfix/main.cf (global settings) and /etc/postfix/master.cf (service daemons).

    Back up the original:

    bash
    sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.bak

    Open main.cf:

    bash
    sudo nano /etc/postfix/main.cf

    Replace its contents with:

    text
    # Basic identity
    myhostname = mail.example.com
    mydomain = example.com
    myorigin = $mydomain
    smtpd_banner = $myhostname ESMTP
    biff = no
    append_dot_mydomain = no
    readme_directory = no
    compatibility_level = 3.6

    Network

    inet_interfaces = all inet_protocols = ipv4 mydestination = localhost mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128

    Size and limits

    message_size_limit = 52428800 mailbox_size_limit = 0 recipient_delimiter = +

    TLS -- inbound (smtpd)

    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_loglevel = 1 smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

    TLS -- outbound (smtp)

    smtp_tls_security_level = may smtp_tls_loglevel = 1 smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

    SASL (authenticate submission clients 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 broken_sasl_auth_clients = yes

    Recipient restrictions

    smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org

    Virtual delivery via Dovecot LMTP

    virtual_transport = lmtp:unix:private/dovecot-lmtp virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf, mysql:/etc/postfix/mysql-virtual-email2email.cf

    Milters (OpenDKIM + Rspamd -- we'll wire these in later steps)

    milter_default_action = accept milter_protocol = 6 smtpd_milters = inet:127.0.0.1:8891, inet:127.0.0.1:11332 non_smtpd_milters = $smtpd_milters

    Now enable submission (587) and smtps (465) in master.cf. These are the authenticated ports used by mail clients like Thunderbird and iPhone Mail.

    bash
    sudo nano /etc/postfix/master.cf

    Find the submission and smtps entries (they are commented out by default) and replace them with:

    text
    submission inet n       -       y       -       -       smtpd
      -o syslog_name=postfix/submission
      -o smtpd_tls_security_level=encrypt
      -o smtpd_sasl_auth_enable=yes
      -o smtpd_reject_unlisted_recipient=no
      -o smtpd_client_restrictions=permit_sasl_authenticated,reject
      -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
      -o milter_macro_daemon_name=ORIGINATING

    smtps inet n - y - - smtpd -o syslog_name=postfix/smtps -o smtpd_tls_wrappermode=yes -o smtpd_sasl_auth_enable=yes -o smtpd_reject_unlisted_recipient=no -o smtpd_client_restrictions=permit_sasl_authenticated,reject -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o milter_macro_daemon_name=ORIGINATING

    The difference: 587 (submission) starts plaintext and upgrades to TLS via STARTTLS. 465 (smtps) is TLS from the first byte. Both require authentication. Port 25 remains unauthenticated for server-to-server transfer, but it only accepts mail destined for your own domains.

    Step 7: Wire Up the Postfix to MySQL Lookup Files

    Postfix needs four small config files that tell it how to query MariaDB. Create them in /etc/postfix/:

    /etc/postfix/mysql-virtual-mailbox-domains.cf -- does this domain exist?

    text
    user = mailuser
    password = CHANGE_ME_STRONG_PASSWORD
    hosts = 127.0.0.1
    dbname = mailserver
    query = SELECT 1 FROM virtual_domains WHERE name='%s'

    /etc/postfix/mysql-virtual-mailbox-maps.cf -- does this email address have a mailbox?

    text
    user = mailuser
    password = CHANGE_ME_STRONG_PASSWORD
    hosts = 127.0.0.1
    dbname = mailserver
    query = SELECT 1 FROM virtual_users WHERE email='%s'

    /etc/postfix/mysql-virtual-alias-maps.cf -- does this address alias to another?

    text
    user = mailuser
    password = CHANGE_ME_STRONG_PASSWORD
    hosts = 127.0.0.1
    dbname = mailserver
    query = SELECT destination FROM virtual_aliases WHERE source='%s'

    /etc/postfix/mysql-virtual-email2email.cf -- canonical self-mapping (needed so Postfix doesn't try to deliver to the original address after alias rewrite):

    text
    user = mailuser
    password = CHANGE_ME_STRONG_PASSWORD
    hosts = 127.0.0.1
    dbname = mailserver
    query = SELECT email FROM virtual_users WHERE email='%s'

    Lock down permissions -- these files contain database credentials:

    bash
    sudo chown root:postfix /etc/postfix/mysql-virtual-*.cf
    sudo chmod 640 /etc/postfix/mysql-virtual-*.cf

    Test each lookup with postmap:

    bash
    sudo postmap -q "example.com" mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
    sudo postmap -q "[email protected]" mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
    sudo postmap -q "[email protected]" mysql:/etc/postfix/mysql-virtual-alias-maps.cf

    The first two should return 1. The third should return [email protected]. If any return empty, re-check the SQL rows and the password in the config file.

    Reload Postfix to pick up all changes:

    bash
    sudo postfix check
    sudo systemctl reload postfix

    postfix check parses configuration and reports syntax errors -- always run it before reloading.

    Step 8: Configure Dovecot (Mail, Auth, Master, SSL)

    Dovecot's configuration is split across /etc/dovecot/dovecot.conf and /etc/dovecot/conf.d/*.conf. We'll edit five files.

    Create the vmail user for mailbox ownership

    bash
    sudo groupadd -g 5000 vmail
    sudo useradd -g vmail -u 5000 vmail -d /var/mail
    sudo mkdir -p /var/mail/vhosts/example.com
    sudo chown -R vmail:vmail /var/mail

    All mailboxes will be owned by this single system user. Per-mailbox ACLs come from the SQL lookup.

    /etc/dovecot/conf.d/10-mail.conf

    Set Maildir format and location:

    bash
    sudo nano /etc/dovecot/conf.d/10-mail.conf

    Modify these lines (the file has extensive comments -- find and change just these directives):

    text
    mail_location = maildir:/var/mail/vhosts/%d/%n
    mail_privileged_group = mail
    mail_uid = vmail
    mail_gid = vmail

    %d is the domain part, %n is the local part. [email protected] will land in /var/mail/vhosts/example.com/yossef/.

    /etc/dovecot/conf.d/10-auth.conf

    Enable plaintext auth over TLS and the SQL backend:

    text
    disable_plaintext_auth = yes
    auth_mechanisms = plain login

    !include auth-sql.conf.ext

    Make sure the !include auth-system.conf.ext line is commented out with a #.

    /etc/dovecot/conf.d/auth-sql.conf.ext

    text
    passdb {
      driver = sql
      args = /etc/dovecot/dovecot-sql.conf.ext
    }

    userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n }

    /etc/dovecot/dovecot-sql.conf.ext

    This file holds the DB credentials (like the Postfix MySQL configs):

    text
    driver = mysql
    connect = host=127.0.0.1 dbname=mailserver user=mailuser password=CHANGE_ME_STRONG_PASSWORD
    default_pass_scheme = SHA256-CRYPT
    password_query = SELECT email as user, password FROM virtual_users WHERE email='%u';

    Lock it down:

    bash
    sudo chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
    sudo chmod 640 /etc/dovecot/dovecot-sql.conf.ext

    /etc/dovecot/conf.d/10-master.conf

    Expose two Unix sockets: one for Postfix's LMTP delivery, one for Postfix's SASL auth.

    Find the service lmtp block and replace with:

    text
    service lmtp {
      unix_listener /var/spool/postfix/private/dovecot-lmtp {
        mode = 0600
        user = postfix
        group = postfix
      }
    }

    Find the service auth block and add the Postfix auth listener:

    text
    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 }

    /etc/dovecot/conf.d/10-ssl.conf

    Point Dovecot at the Let's Encrypt certificate:

    text
    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_cipher_list = ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:!aNULL:!eNULL:!MD5:!EXPORT:!DSS:!SEED:!3DES:!RC4
    ssl_prefer_server_ciphers = yes

    Restart Dovecot and confirm it's listening:

    bash
    sudo systemctl restart dovecot
    sudo ss -tlnp | grep dovecot

    Expected output:

    text
    LISTEN 0 100 0.0.0.0:993  ...  users:(("dovecot",...))
    LISTEN 0 100 0.0.0.0:995  ...
    LISTEN 0 100 0.0.0.0:143  ...
    LISTEN 0 100 0.0.0.0:110  ...

    Ports 993 (IMAPS) and 995 (POP3S) are the ones your clients will connect on.

    Step 9: Install and Configure OpenDKIM

    DKIM signs every outbound message with a cryptographic signature that the receiver verifies against a public key published in your DNS. Without DKIM, Gmail will treat your mail with suspicion -- often marking it as spam even with valid SPF.

    Create the directory structure and key:

    bash
    sudo mkdir -p /etc/opendkim/keys/example.com
    cd /etc/opendkim/keys/example.com
    sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s mail -v
    sudo chown -R opendkim:opendkim /etc/opendkim
    sudo chmod 600 /etc/opendkim/keys/example.com/mail.private

    This creates two files:

    • mail.private -- the private key (stays on the server)
    • mail.txt -- the DNS TXT record you need to publish
    View the public key:

    bash
    sudo cat /etc/opendkim/keys/example.com/mail.txt

    You'll see something like:

    text
    mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; "
      "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
      "...QIDAQAB" )

    Concatenate the quoted strings into a single line and publish as a TXT record at mail._domainkey.example.com in your DNS panel:

    TypeNameValue
    TXTmail._domainkey.example.comv=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...QIDAQAB
    Configure OpenDKIM. Edit /etc/opendkim.conf:

    text
    Syslog                  yes
    UMask                   007
    Mode                    sv
    Canonicalization        relaxed/simple
    SubDomains              no
    AutoRestart             yes
    AutoRestartRate         10/1h
    Socket                  inet:[email protected]
    PidFile                 /run/opendkim/opendkim.pid
    UserID                  opendkim:opendkim

    KeyTable /etc/opendkim/key.table SigningTable refile:/etc/opendkim/signing.table ExternalIgnoreList /etc/opendkim/trusted.hosts InternalHosts /etc/opendkim/trusted.hosts

    Create the lookup tables:

    bash
    sudo tee /etc/opendkim/key.table > /dev/null <<'EOF'
    mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com/mail.private
    EOF

    sudo tee /etc/opendkim/signing.table > /dev/null <<'EOF' *@example.com mail._domainkey.example.com EOF

    sudo tee /etc/opendkim/trusted.hosts > /dev/null <<'EOF' 127.0.0.1 localhost mail.example.com example.com EOF

    sudo chown -R opendkim:opendkim /etc/opendkim

    Start the service:

    bash
    sudo systemctl enable --now opendkim
    sudo systemctl status opendkim

    Postfix already has smtpd_milters = inet:127.0.0.1:8891, ... configured from Step 6. Verify the signature is being added after you restart Postfix (see Step 11).

    Test your DKIM record once DNS propagates:

    bash
    opendkim-testkey -d example.com -s mail -vvv

    Expected output:

    text
    opendkim-testkey: key OK

    Step 10: Install Rspamd and Hook It Into Postfix

    Rspamd is the modern replacement for SpamAssassin. It runs as a daemon, uses modern statistical classifiers, and integrates with Postfix via the milter protocol on port 11332.

    Rspamd is already installed from Step 3. Configure it to act as a milter:

    bash
    sudo tee /etc/rspamd/local.d/milter_headers.conf > /dev/null <<'EOF'
    use = ["authentication-results", "x-spam-status", "x-spam-score"];
    EOF

    sudo tee /etc/rspamd/local.d/worker-proxy.inc > /dev/null <<'EOF' milter = yes; timeout = 120s; upstream "local" { default = yes; self_scan = yes; } EOF

    sudo tee /etc/rspamd/local.d/actions.conf > /dev/null <<'EOF' reject = 15; add_header = 6; greylist = 4; EOF

    Set a password for the Rspamd web UI (accessible at http://localhost:11334):

    bash
    rspamadm pw

    Copy the generated hash and put it in /etc/rspamd/local.d/worker-controller.inc:

    text
    password = "$2$abc123...";

    Restart Rspamd:

    bash
    sudo systemctl restart rspamd

    Postfix's smtpd_milters from Step 6 already includes inet:127.0.0.1:11332, so Rspamd is already wired into the mail flow. You can confirm by checking the headers of a test message -- they'll include X-Spam-Status and Authentication-Results.

    Access the Rspamd dashboard by tunneling through SSH:

    bash
    ssh -L 11334:localhost:11334 [email protected]

    Then open http://localhost:11334 in your browser.

    Step 11: Test Send and Receive

    Now the moment of truth. Restart everything:

    bash
    sudo systemctl restart postfix dovecot opendkim rspamd

    Check the logs in one terminal:

    bash
    sudo tail -f /var/log/mail.log

    Test 1: Receive a message from Gmail

    From your personal Gmail account, send a message to [email protected]. Watch mail.log. You should see lines like:

    text
    postfix/smtpd[...]: connect from mail-sor-f41.google.com[209.85.220.41]
    postfix/smtpd[...]: ... client=mail-sor-f41.google.com[209.85.220.41]
    postfix/cleanup[...]: ... message-id=<...>
    postfix/qmgr[...]: ... from=<[email protected]>, size=..., nrcpt=1
    dovecot: lmtp(...): ... msgid=<...>: sieve: stored mail into mailbox 'INBOX'
    postfix/lmtp[...]: ... status=sent

    Verify the message landed:

    bash
    sudo ls -la /var/mail/vhosts/example.com/yossef/new/

    You should see a new file with a Maildir-format name.

    Test 2: IMAP login from a mail client

    Configure Thunderbird, iPhone Mail, or Apple Mail:

    • IMAP server: mail.example.com, port 993, SSL/TLS
    • SMTP server: mail.example.com, port 587, STARTTLS
    • Username: [email protected] (full email address)
    • Password: the one you hashed into the database
    The client should connect and show the message from Test 1.

    Test 3: Send outbound and check deliverability

    Send a test message from your mail client to mail-tester.com. Visit the reported address, send mail to it, then go back to the mail-tester page to see your score.

    A correctly configured server on a clean IP scores 9.5-10/10. Common deductions:

    • SPF -- missing record on the sending domain.
    • DKIM -- signature not present or doesn't validate (DNS not propagated, wrong selector).
    • DMARC -- no record at _dmarc.example.com.
    • rDNS -- PTR doesn't match HELO.
    • IP reputation -- VPS IP previously used for spam. Check against MXToolbox blacklists.

    Test 4: Validate DNS from the outside

    Run a final full check:

    bash
    # Message headers from any delivered mail should show:
    

    Authentication-Results: spf=pass; dkim=pass; dmarc=pass

    From outside the server, run:

    dig +short MX example.com dig +short TXT example.com dig +short TXT _dmarc.example.com dig +short TXT mail._domainkey.example.com dig +short -x YOUR_VPS_IP

    All five should return their expected values. If DKIM doesn't show, DNS hasn't propagated yet -- wait up to an hour.

    Troubleshooting

    ProblemCauseSolution
    SASL authentication failed in clientWrong username/password, auth socket not wired correctlyVerify smtpd_sasl_path = private/auth in main.cf. Check /var/spool/postfix/private/auth exists and is owned by postfix:postfix. Regenerate the password hash with doveadm pw -s SHA256-CRYPT and update the DB.
    DKIM signature verification failed (bad signature)DNS TXT record doesn't exactly match the key, or the key was regenerated after publishingsudo opendkim-testkey -d example.com -s mail -vvv. If it reports "record not found," DNS hasn't propagated. If "key mismatch," re-paste the TXT value -- watch for line breaks and missing characters.
    TLS handshake failed on port 587Certificate path wrong or file permissions block the postfix userRun sudo postfix check. Confirm /etc/letsencrypt/live/mail.example.com/ exists. Ensure the symlink targets in /etc/letsencrypt/archive/ are readable by root -- Postfix reads them as root at startup.
    Mail rejected with Recipient address rejected: User unknown in virtual mailbox tableSQL lookup isn't finding the usersudo postmap -q "[email protected]" mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf. If empty, the row isn't in virtual_users or the mailuser DB password doesn't match the one in the Postfix config.
    connect to 127.0.0.1:8891: Connection refused in logsOpenDKIM not running or not listening on the expected portsudo systemctl status opendkim. Check Socket inet:[email protected] is set in /etc/opendkim.conf. Restart opendkim.
    Gmail marks all mail as spamMissing DKIM, bad rDNS, or IP on blocklistRun the message through mail-tester.com. Check the IP at mxtoolbox.com/blacklists.aspx. If blocklisted, request delisting or move to a clean IP.
    Port 25 connections time out from outsideVPS provider blocks outbound port 25Open a support ticket with your VPS provider asking for port 25 unblocking. Most providers allow it after a brief review.
    Relay access denied when sending from a mail clientClient isn't authenticating, or smtpd_relay_restrictions blocks itConfirm the client is using port 587 or 465 with auth enabled. Check master.cf submission block includes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject.
    IMAP shows no messages despite successful deliveryWrong mail_location, or Maildir permissions wrongsudo ls -la /var/mail/vhosts/example.com/yossef/new/. Should be owned by vmail:vmail. Fix with sudo chown -R vmail:vmail /var/mail/vhosts.
    Rspamd rejects legitimate mailAggressive default thresholdsLower the reject action score in /etc/rspamd/local.d/actions.conf. Train the Bayesian classifier: rspamc learn_ham /path/to/known-good.eml and rspamc learn_spam /path/to/known-spam.eml.

    Essential Log Commands

    bash
    # Live mail log (most useful)
    sudo tail -f /var/log/mail.log

    Postfix queue -- pending and deferred messages

    sudo postqueue -p

    Flush the queue (try to redeliver now)

    sudo postqueue -f

    OpenDKIM logs

    sudo journalctl -u opendkim -f

    Rspamd logs

    sudo journalctl -u rspamd -f

    Dovecot logs

    sudo journalctl -u dovecot -f

    FAQ

    Do I really need DKIM, SPF, and DMARC all three?

    Yes, if you want mail delivered. SPF tells receivers which IPs are allowed to send for your domain. DKIM cryptographically proves the message wasn't altered in transit and came from your server. DMARC tells receivers what to do when SPF or DKIM fail and lets you collect aggregate reports about who is sending mail as your domain. Gmail, Outlook, and Yahoo have moved to requiring all three for bulk senders -- without them, your mail goes to spam or gets rejected outright. For details on each, see the specs at datatracker.ietf.org (SPF), rfc6376 (DKIM), and rfc7489 (DMARC).

    Why is my outbound mail blocked even with perfect DNS?

    IP reputation. A brand-new VPS IP has no sending history; some receivers treat that as suspicious until you've warmed it up. Send small volumes (under 50 messages/day) for the first two weeks. Encourage recipients to reply -- replies are a strong positive signal. Monitor the IP on mxtoolbox.com/blacklists.aspx and request delisting from any blocklist that catches you. If the IP came from a provider known for abuse, your cheapest fix is migrating to a cleaner IP block -- our CloudCore plans ship with reputation-checked IPs.

    Can I add a webmail interface?

    Yes. The two most common choices are Roundcube (PHP, lightweight, widely packaged) and SnappyMail (PHP, modern UI, faster for large mailboxes). Install Roundcube with sudo apt install roundcube roundcube-mysql, point it at localhost:143 for IMAP and localhost:587 for SMTP, then expose it behind Nginx with TLS. The setup is straightforward and adds a browser-based inbox without touching the mail stack.

    How do I add more domains?

    Add rows to virtual_domains and virtual_users in MariaDB. You also need to generate a new DKIM key per domain (sudo opendkim-genkey -b 2048 -d newdomain.com -D /etc/opendkim/keys/newdomain.com -s mail), add an entry to /etc/opendkim/key.table and signing.table, and publish the new DKIM TXT record. Postfix doesn't need reconfiguring because the MySQL lookup handles routing automatically. Dovecot also needs no changes because mail_location uses the %d placeholder.

    How does manual Postfix compare to Mailcow or Mailu?

    Postfix + Dovecot (this guide) -- minimal, fast, deep customization. You maintain each component separately with apt. Best for learners and operators who want control.

    Mailcow -- Docker-based stack with SOGo webmail, ClamAV, Rspamd, a polished admin UI, and push sync. Runs in ~15 containers and needs 4-6 GB of RAM. Best for running mail for multiple users without touching config files.

    Mailu -- Docker-based but lighter than Mailcow, with Roundcube or SnappyMail webmail. ~8 containers, ~2 GB RAM. Middle ground between manual and Mailcow.

    For a single-operator VPS, manual is perfectly sustainable. When you hit 3+ domains with 20+ users each, Mailcow's UI starts paying for itself.

    Can I send transactional mail from my apps through this server?

    Yes, but you'll usually get better deliverability from a dedicated relay (Amazon SES, Postmark, Mailgun) because they manage IP reputation at scale. If volume is low (under 1000 messages/day) and you maintain good list hygiene, a self-hosted Postfix is fine. Point your app at mail.example.com:587 with SASL credentials and you're done. For higher volumes, configure Postfix as a null-client relay that forwards through SES using the relayhost directive.

    Next Steps

    Now that Postfix and Dovecot are running, here are high-value follow-ups:

    • Add Roundcube webmail -- Install roundcube and expose it behind Nginx with a Let's Encrypt cert. Your users get a browser-based inbox at https://webmail.example.com without any client configuration. See our Nginx reverse proxy guide for the vhost setup.
    • Harden the firewall with UFW -- Restrict inbound to ports 25, 465, 587, 993, 995, 443, 80, and 22 only. Our UFW configuration guide shows the exact rules and how to rate-limit SSH.
    • Monitor with Uptime Kuma -- Deploy Uptime Kuma to check SMTP and IMAP uptime from outside your network. Configure alerts for port 25 outbound blocks and certificate expiry.
    • Automate DMARC aggregate report parsing -- Tools like parsedmarc ingest the XML reports that receivers send to your rua= address and visualize them in Grafana. You'll spot deliverability problems and spoofing attempts long before users complain.
    • Train the Bayesian classifier -- Regularly feed Rspamd examples of known ham and known spam with rspamc learn_ham and rspamc learn_spam. Accuracy improves dramatically after a few hundred samples.
    • Read the official docs -- postfix.org/documentation.html and dovecot.org/documentation.html are dense but authoritative. The Postfix BASIC_CONFIGURATION_README and Dovecot wiki2 pages are the right places to go when you want to go beyond this guide.

    Run Your Mail Server on a Clean-IP VPS
    >
    Mail deliverability is half DNS and half IP reputation. Our CloudCore Professional plans come with reputation-checked IPs, panel-based reverse DNS, and full port 25 access out of the box.
    >
    - 6 vCPU cores and 12 GB RAM -- enough for Postfix, Dovecot, Rspamd, and a webmail UI
    - 100 GB NVMe SSD for fast Maildir access
    - rDNS configurable from the control panel in two clicks
    - Port 25 open by default -- no support ticket needed
    - Clean IP blocks with no prior spam history
    >
    Deploy Your Mail VPS Now -- Plans start at EUR 19.99/month.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket