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
sudoprivileges. - A domain (
your_domain) with these DNS records: an A record formail.your_domainpointing toyour_server_ip, an MX record foryour_domainpointing tomail.your_domain, an SPF record (v=spf1 mx ~all) and a DMARC record at_dmarc.your_domain. - The reverse DNS (PTR) of
your_server_ipset tomail.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:
| Traffic | Port | Handled by |
|---|---|---|
| Mail from other servers | 25 | Postfix smtpd, then LMTP to Dovecot |
| Mail sent by your users | 587 (STARTTLS) | Postfix smtpd, password checked by Dovecot SASL |
| Mail clients reading mail | 993 (IMAPS) | Dovecot IMAP |
| Postfix to Dovecot delivery | Unix socket private/dovecot-lmtp | Dovecot LMTP |
| Postfix to Dovecot authentication | Unix socket private/auth | Dovecot 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=encryptrequires STARTTLS before any other command.smtpd_sasl_auth_enable=yesturns on authentication only on this port. Port 25 never accepts passwords.smtpd_sender_login_mapswithreject_sender_login_mismatchstops an authenticated user from sending as someone else. Becausevmailboxmaps each address to itself,admin@your_domaincan only send asadmin@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:
- Add a line
user@your_domain:{SHA512-CRYPT}...to/etc/dovecot/users. Dovecot notices the change on its own. - Add
user@your_domain user@your_domainto/etc/postfix/vmailboxand runsudo postmap /etc/postfix/vmailbox.
To add another domain, example.net for instance:
- Create its A, MX, SPF and DMARC records.
- Add
example.net OKto/etc/postfix/vdomainsand runsudo postmap /etc/postfix/vdomains. - Add its
postmasterandabusealiases to/etc/postfix/virtualand runsudo postmap /etc/postfix/virtual. - 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 theservice lmtpblock in10-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 invmailboxbut 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 runpostmapafter editing it.Error: open(/var/mail/vhosts/...) failed: Permission denied:/var/mail/vhostsmust be owned byvmail:vmail. Fix it withsudo chown -R vmail:vmail /var/mail/vhosts.SASL LOGIN authentication failedin the Postfix log with a correct password: check the Dovecot side withsudo doveadm auth test user@your_domainand readsudo grep 'auth' /var/log/mail.log | tail.- Outgoing mail stays in the queue with
Connection timed out: outbound port 25 is blocked. Check withnc -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.
