Postfix and Dovecot together make a complete mail server: Postfix exchanges mail with other servers over SMTP, and Dovecot stores it, serves it to mail clients over IMAP and checks passwords for both. In this tutorial you will build that stack on Ubuntu 24.04 with virtual mailboxes, meaning mail accounts such as admin@your_domain that are not Linux users. Postfix hands incoming mail to Dovecot over LMTP, Dovecot authenticates clients for IMAP and for sending on port 587, and adding a user or a whole domain is a matter of editing two text files.

Prerequisites

To follow this tutorial you need:

  • A fresh server running Ubuntu 24.04 LTS with a public IPv4 address, for example a CubePath VPS, and a non-root user with sudo privileges.
  • A domain (your_domain) with these DNS records: an A record for mail.your_domain pointing to your_server_ip, an MX record for your_domain pointing to mail.your_domain, an SPF record (v=spf1 mx ~all) and a DMARC record at _dmarc.your_domain.
  • The reverse DNS (PTR) of your_server_ip set to mail.your_domain, through your hosting provider.
  • Outbound TCP port 25 open. On CubePath, as with most cloud providers, it is closed by default; request it through a support ticket that describes your mail use.

The Postfix setup guide on this site ("How to Set Up a Postfix Mail Server on Ubuntu 24.04") explains the DNS records, the port 25 check and DKIM signing in detail. This tutorial focuses on the parts where Postfix and Dovecot connect.

Here is how the pieces fit together:

TrafficPortHandled by
Mail from other servers25Postfix smtpd, then LMTP to Dovecot
Mail sent by your users587 (STARTTLS)Postfix smtpd, password checked by Dovecot SASL
Mail clients reading mail993 (IMAPS)Dovecot IMAP
Postfix to Dovecot deliveryUnix socket private/dovecot-lmtpDovecot LMTP
Postfix to Dovecot authenticationUnix socket private/authDovecot auth

Step 1 - Setting the hostname and getting a certificate

Set the server's hostname to the name in your MX record:

sudo hostnamectl set-hostname mail.your_domain

Add the name to /etc/hosts so it resolves locally:

sudo nano /etc/hosts
127.0.1.1 mail.your_domain mail

Enable the firewall with SSH and the ports this server needs. Port 80 is used by Certbot to prove control of the domain:

sudo ufw allow OpenSSH
sudo ufw allow 25/tcp
sudo ufw allow 80/tcp
sudo ufw allow 587/tcp
sudo ufw allow 993/tcp
sudo ufw enable

Install Certbot and get a certificate for mail.your_domain in standalone mode:

sudo apt update
sudo apt install certbot
sudo certbot certonly --standalone -d mail.your_domain

Postfix and Dovecot read the certificate only when they start, so reload them after each renewal. Certbot runs every executable in its deploy hooks directory after a successful renewal:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh
#!/bin/sh
systemctl reload postfix dovecot
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-mail.sh

Verify that renewal works:

sudo certbot renew --dry-run

Step 2 - Installing Postfix and Dovecot

Install Postfix, the Dovecot core, the IMAP server and the LMTP server:

sudo apt install postfix dovecot-core dovecot-imapd dovecot-lmtpd

When the Postfix installer asks, choose Internet Site and enter mail.your_domain as the System mail name. In a virtual mailbox setup, your_domain is served by Dovecot, so it must not be treated as the local system domain.

Check that both services are running:

systemctl is-active postfix dovecot
active
active

Step 3 - Creating the mail storage user

All virtual mailboxes are stored under one directory and owned by a single unprivileged system user, vmail. Create the group and user with a fixed ID of 5000:

sudo groupadd --system --gid 5000 vmail
sudo useradd --system --uid 5000 --gid vmail --home-dir /var/mail/vhosts --no-create-home --shell /usr/sbin/nologin vmail

Create the storage directory and give it to vmail:

sudo mkdir -p /var/mail/vhosts
sudo chown vmail:vmail /var/mail/vhosts
sudo chmod 770 /var/mail/vhosts

Mail for user@domain will be stored in /var/mail/vhosts/domain/user/ in Maildir format. Dovecot creates the per-domain and per-user folders on first delivery.

Step 4 - Configuring Postfix for virtual domains

Back up the main configuration file:

sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.orig

Set the hostname and keep mydestination limited to the machine's own names. Listing your_domain here would make Postfix deliver it to Linux users instead of Dovecot:

sudo postconf -e "myhostname = mail.your_domain"
sudo postconf -e "mydestination = \$myhostname, localhost.localdomain, localhost"

Tell Postfix which domains and addresses are virtual, and to deliver them to Dovecot over LMTP. The socket path is relative to the Postfix spool directory /var/spool/postfix:

sudo postconf -e "virtual_mailbox_domains = hash:/etc/postfix/vdomains"
sudo postconf -e "virtual_mailbox_maps = hash:/etc/postfix/vmailbox"
sudo postconf -e "virtual_alias_maps = hash:/etc/postfix/virtual"
sudo postconf -e "virtual_transport = lmtp:unix:private/dovecot-lmtp"

Use Dovecot for SASL authentication:

sudo postconf -e "smtpd_sasl_type = dovecot"
sudo postconf -e "smtpd_sasl_path = private/auth"

Configure TLS with the Let's Encrypt certificate. may offers STARTTLS on port 25 without refusing the rare server that cannot use it:

sudo postconf -e "smtpd_tls_cert_file = /etc/letsencrypt/live/mail.your_domain/fullchain.pem"
sudo postconf -e "smtpd_tls_key_file = /etc/letsencrypt/live/mail.your_domain/privkey.pem"
sudo postconf -e "smtpd_tls_security_level = may"
sudo postconf -e "smtp_tls_security_level = may"

Add basic checks on incoming connections:

sudo postconf -e "smtpd_helo_required = yes"
sudo postconf -e "disable_vrfy_command = yes"
sudo postconf -e "smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname"
sudo postconf -e "smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_sender, reject_unknown_sender_domain"

Ubuntu's default smtpd_relay_restrictions (permit_mynetworks permit_sasl_authenticated defer_unauth_destination) already blocks open relaying, so leave it unchanged.

Creating the lookup tables

Create the list of virtual domains. The value after the domain is ignored, but a value is required:

sudo nano /etc/postfix/vdomains
your_domain    OK

Create the list of mailboxes. Each address maps to itself: virtual_mailbox_maps only needs the key to exist, and the value is reused in Step 6 to decide which login may send as which address:

sudo nano /etc/postfix/vmailbox
admin@your_domain    admin@your_domain

Create the aliases. Every domain needs postmaster and abuse, and here both go to admin:

sudo nano /etc/postfix/virtual
postmaster@your_domain    admin@your_domain
abuse@your_domain         admin@your_domain

Postfix reads compiled versions of these files. Build them with postmap:

sudo postmap /etc/postfix/vdomains
sudo postmap /etc/postfix/vmailbox
sudo postmap /etc/postfix/virtual

Route mail for local system accounts such as root (cron jobs, security updates) to the admin mailbox:

sudo nano /etc/aliases
postmaster: root
root: admin@your_domain
sudo newaliases

Step 5 - Enabling the submission port

Users send mail through port 587, where authentication is mandatory. Open the Postfix service table:

sudo nano /etc/postfix/master.cf

Ubuntu's file contains a commented-out submission block. Replace it with this one. Lines that start with -o must be indented:

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_sender_login_maps=hash:/etc/postfix/vmailbox
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_sender_restrictions=reject_sender_login_mismatch
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

What these options do:

  • smtpd_tls_security_level=encrypt requires STARTTLS before any other command.
  • smtpd_sasl_auth_enable=yes turns on authentication only on this port. Port 25 never accepts passwords.
  • smtpd_sender_login_maps with reject_sender_login_mismatch stops an authenticated user from sending as someone else. Because vmailbox maps each address to itself, admin@your_domain can only send as admin@your_domain.

Check the Postfix configuration:

sudo postfix check

No output means no errors. Postfix is restarted in Step 7, once Dovecot provides the sockets it needs.

Step 6 - Configuring Dovecot

Dovecot's configuration lives in /etc/dovecot/conf.d/. You will change five files.

Mail location

Open the mail settings:

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

Set the location to the virtual mailbox tree. %d is the domain and %n the user part of the login:

mail_location = maildir:/var/mail/vhosts/%d/%n

Authentication

Open the authentication settings:

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

Set the options below. At the end of the file, comment out the system user backend and enable the password file backend:

disable_plaintext_auth = yes
auth_mechanisms = plain login

#!include auth-system.conf.ext
!include auth-passwdfile.conf.ext

Now define that backend. Open its file and replace the whole contents:

sudo nano /etc/dovecot/conf.d/auth-passwdfile.conf.ext
passdb {
  driver = passwd-file
  args = scheme=SHA512-CRYPT username_format=%u /etc/dovecot/users
}

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

The passdb checks passwords against /etc/dovecot/users, keyed by the full address (%u). The userdb gives every account the same system owner, vmail, and a home directory derived from the address, so you do not have to repeat that for each user. Dovecot still uses the password file to verify that a user exists before accepting mail for it.

TLS

Open the SSL settings:

sudo nano /etc/dovecot/conf.d/10-ssl.conf
ssl = required
ssl_cert = </etc/letsencrypt/live/mail.your_domain/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.your_domain/privkey.pem
ssl_min_protocol = TLSv1.2

The < before each path tells Dovecot to read the file's content.

Sockets for Postfix

Open the service definitions:

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

Replace the service lmtp block so the LMTP socket is created inside the Postfix spool, where Postfix's chrooted processes can reach it:

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

In the service auth block, uncomment and complete the Postfix authentication socket:

service auth {
  unix_listener auth-userdb {
    #mode = 0666
    #user =
    #group =
  }

  # Postfix smtp-auth
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}

Default folders

Open the mailbox definitions:

sudo nano /etc/dovecot/conf.d/15-mailboxes.conf

Add auto = subscribe to the Drafts, Junk, Trash and Sent blocks so every new account gets them. For example:

  mailbox Sent {
    auto = subscribe
    special_use = \Sent
  }

Step 7 - Creating the first mailbox and starting the services

Generate a password hash for admin@your_domain. The command asks for the password twice and prints the hash:

sudo doveadm pw -s SHA512-CRYPT
Enter new password:
Retype new password:
{SHA512-CRYPT}$6$Xk2Qb0yY3m6n5R9s$Qm8...

Create the password file:

sudo nano /etc/dovecot/users

Add one line per mailbox: the full address, a colon, and the hash you just generated:

admin@your_domain:{SHA512-CRYPT}$6$Xk2Qb0yY3m6n5R9s$Qm8...

Only Dovecot should be able to read it:

sudo chown root:dovecot /etc/dovecot/users
sudo chmod 640 /etc/dovecot/users

Check the Dovecot configuration. doveconf -n exits with an error message if any file has a mistake:

sudo doveconf -n > /dev/null && echo "Dovecot config OK"

Restart Dovecot first, so the sockets exist, then Postfix:

sudo systemctl restart dovecot
sudo systemctl restart postfix

Confirm that both sockets were created with the right owner:

sudo ls -l /var/spool/postfix/private/auth /var/spool/postfix/private/dovecot-lmtp
srw-rw---- 1 postfix postfix 0 Sep 25 12:10 /var/spool/postfix/private/auth
srw------- 1 postfix postfix 0 Sep 25 12:10 /var/spool/postfix/private/dovecot-lmtp

Step 8 - Testing the complete flow

Test each link of the chain separately, so a failure points to one component.

Authentication and user lookup

Check the password and the user data Dovecot will use:

sudo doveadm auth test admin@your_domain
sudo doveadm user admin@your_domain
passdb: admin@your_domain auth succeeded
extra fields:
  user=admin@your_domain

field	value
uid	5000
gid	5000
home	/var/mail/vhosts/your_domain/admin

Local delivery through LMTP

Send a message to the postmaster alias. Postfix should expand it to admin@your_domain and hand it to Dovecot:

printf 'Subject: LMTP test\n\nDelivery works.\n' | sendmail postmaster@your_domain

Check the log:

sudo grep -E 'lmtp|saved mail' /var/log/mail.log | tail -n 3
postfix/lmtp[7021]: 4XbS0q2Hc1z9sWg: to=<admin@your_domain>, orig_to=<postmaster@your_domain>, relay=mail.your_domain[private/dovecot-lmtp], delay=0.08, dsn=2.0.0, status=sent (250 2.0.0 <admin@your_domain> 8cB2FIr7c2ZtGwAAqL9Tyg Saved)
dovecot: lmtp(admin@your_domain)<7023><8cB2FIr7c2ZtGwAAqL9Tyg>: msgid=<[email protected]_domain>: saved mail to INBOX

Confirm the message count in the mailbox:

sudo doveadm mailbox status -u admin@your_domain messages INBOX
INBOX messages=1

Reading mail over IMAP

Open an IMAP session over TLS and log in with the full address. Replace your_password with the real password:

openssl s_client -connect mail.your_domain:993 -quiet
a LOGIN admin@your_domain your_password
b SELECT INBOX
c LOGOUT
a OK [CAPABILITY ...] Logged in
* 1 EXISTS
...
b OK [READ-WRITE] Select completed.

Sending through the submission port

Install swaks, a command-line SMTP test tool, and send a message to an external mailbox through port 587. It asks for the password:

sudo apt install swaks
swaks --to [email protected] --from admin@your_domain --server mail.your_domain --port 587 --tls --auth LOGIN --auth-user admin@your_domain
<-  250 2.0.0 Ok: queued as 4XbS3d5Kq1z9sWh
 -> QUIT
<-  221 2.0.0 Bye

Try sending as an address you do not own, to confirm the sender check works:

swaks --to [email protected] --from ceo@your_domain --server mail.your_domain --port 587 --tls --auth LOGIN --auth-user admin@your_domain
<** 553 5.7.1 <ceo@your_domain>: Sender address rejected: not owned by user admin@your_domain

Finally, send a message from Gmail or Outlook to admin@your_domain and check that it arrives with doveadm mailbox status as above. This proves that the MX record, port 25 and LMTP delivery work end to end.

Step 9 - Adding users and domains

To add a mailbox, generate a hash with sudo doveadm pw -s SHA512-CRYPT, then:

  1. Add a line user@your_domain:{SHA512-CRYPT}... to /etc/dovecot/users. Dovecot notices the change on its own.
  2. Add user@your_domain user@your_domain to /etc/postfix/vmailbox and run sudo postmap /etc/postfix/vmailbox.

To add another domain, example.net for instance:

  1. Create its A, MX, SPF and DMARC records.
  2. Add example.net OK to /etc/postfix/vdomains and run sudo postmap /etc/postfix/vdomains.
  3. Add its postmaster and abuse aliases to /etc/postfix/virtual and run sudo postmap /etc/postfix/virtual.
  4. Add its mailboxes as described above.

Postfix detects rebuilt hash: tables automatically, so no reload is needed. To remove a mailbox, delete its lines from both files, run postmap again, and delete /var/mail/vhosts/domain/user once you no longer need the mail.

Step 10 - Blocking password guessing with Fail2ban

Both port 587 and port 993 will receive password-guessing attempts. Install Fail2ban:

sudo apt install fail2ban

Enable its built-in Dovecot and Postfix SASL jails:

sudo nano /etc/fail2ban/jail.d/mail.local
[dovecot]
enabled = true

[postfix-sasl]
enabled = true

Restart the service and check that the jails are active:

sudo systemctl restart fail2ban
sudo fail2ban-client status
Status
|- Number of jail:      3
`- Jail list:   dovecot, postfix-sasl, sshd

Troubleshooting

  • connect to transport private/dovecot-lmtp: No such file or directory: the LMTP socket is missing. Check the service lmtp block in 10-master.conf, restart Dovecot and list /var/spool/postfix/private/.
  • status=bounced (... User doesn't exist: user@your_domain): Postfix accepted the address but Dovecot does not know it. The address is in vmailbox but missing from /etc/dovecot/users, or the two are spelled differently.
  • Recipient address rejected: User unknown in virtual mailbox table: the address is missing from /etc/postfix/vmailbox, or you forgot to run postmap after editing it.
  • Error: open(/var/mail/vhosts/...) failed: Permission denied: /var/mail/vhosts must be owned by vmail:vmail. Fix it with sudo chown -R vmail:vmail /var/mail/vhosts.
  • SASL LOGIN authentication failed in the Postfix log with a correct password: check the Dovecot side with sudo doveadm auth test user@your_domain and read sudo grep 'auth' /var/log/mail.log | tail.
  • Outgoing mail stays in the queue with Connection timed out: outbound port 25 is blocked. Check with nc -vz -w 5 gmail-smtp-in.l.google.com 25.

Conclusion

You now have a mail server where Postfix accepts and relays mail, Dovecot stores it in per-domain Maildir folders and authenticates both IMAP and SMTP users, and new mailboxes or domains are added by editing two files. Before using it for real mail, add DKIM signing with OpenDKIM as described in the Postfix setup guide, since Gmail and Outlook expect it. After that, consider spam filtering with Rspamd, mailbox quotas and Sieve filtering with dovecot-sieve, and regular backups of /var/mail/vhosts, /etc/postfix, /etc/dovecot and /etc/opendkim.