Postfix virtual domains allow a single mail server to handle email for multiple domains with separate user namespaces. Unlike system accounts, virtual users do not need Linux accounts and can be managed through database lookups. This guide covers configuring Postfix with virtual mailbox domains using either file-based or database-backed user management.
Understanding Virtual vs. Local Delivery
- Local delivery — maps to Linux system accounts; limited to one domain
- Virtual alias domains — maps addresses to other addresses (forwarding only)
- Virtual mailbox domains — stores mail in mailboxes without requiring system accounts
File-Based Virtual Domains
Configure Virtual Mailbox Domains
# /etc/postfix/main.cf
virtual_mailbox_domains = example.com, example.org, example.net
virtual_mailbox_base = /var/mail/vhosts
virtual_mailbox_maps = hash:/etc/postfix/vmailbox
virtual_alias_maps = hash:/etc/postfix/virtual
virtual_minimum_uid = 1000
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
Create Virtual Mailbox User
# Create the vmail system user
sudo groupadd -g 5000 vmail
sudo useradd -g vmail -u 5000 -d /var/mail/vhosts -s /usr/sbin/nologin vmail
sudo mkdir -p /var/mail/vhosts
sudo chown -R vmail:vmail /var/mail/vhosts
Define Mailboxes
# /etc/postfix/vmailbox
# Format: email_address mailbox_path (relative to virtual_mailbox_base)
user1@example.com example.com/user1/
user2@example.com example.com/user2/
admin@example.org example.org/admin/
info@example.net example.net/info/
Define Aliases
# /etc/postfix/virtual
# Format: alias_address target_address
postmaster@example.com admin@example.com
abuse@example.com admin@example.com
support@example.com user1@example.com, user2@example.com
@example.org catchall@example.org
# Build hash maps and reload
sudo postmap /etc/postfix/vmailbox
sudo postmap /etc/postfix/virtual
sudo systemctl reload postfix
Database-Backed Virtual Domains (MySQL)
For larger deployments, store domains, users, and aliases in MySQL:
Create Database Schema
CREATE DATABASE mailserver;
CREATE TABLE domains (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
active BOOLEAN DEFAULT TRUE
);
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
domain_id INT NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
maildir VARCHAR(255) NOT NULL,
quota BIGINT DEFAULT 0,
active BOOLEAN DEFAULT TRUE,
FOREIGN KEY (domain_id) REFERENCES domains(id)
);
CREATE TABLE aliases (
id INT AUTO_INCREMENT PRIMARY KEY,
domain_id INT NOT NULL,
source VARCHAR(255) NOT NULL,
destination TEXT NOT NULL,
active BOOLEAN DEFAULT TRUE,
FOREIGN KEY (domain_id) REFERENCES domains(id)
);
-- Insert example data
INSERT INTO domains (name) VALUES ('example.com'), ('example.org');
INSERT INTO users (domain_id, email, password, maildir) VALUES
(1, 'user@example.com', '$6$hashed_password', 'example.com/user/'),
(2, 'admin@example.org', '$6$hashed_password', 'example.org/admin/');
Configure Postfix MySQL Lookups
# /etc/postfix/mysql-virtual-domains.cf
user = mailserver
password = MailDBPass123
hosts = 127.0.0.1
dbname = mailserver
query = SELECT name FROM domains WHERE name='%s' AND active = TRUE
# /etc/postfix/mysql-virtual-mailboxes.cf
user = mailserver
password = MailDBPass123
hosts = 127.0.0.1
dbname = mailserver
query = SELECT maildir FROM users WHERE email='%s' AND active = TRUE
# /etc/postfix/mysql-virtual-aliases.cf
user = mailserver
password = MailDBPass123
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM aliases WHERE source='%s' AND active = TRUE
Update Postfix Configuration
# /etc/postfix/main.cf
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailboxes.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-aliases.cf
virtual_mailbox_base = /var/mail/vhosts
virtual_minimum_uid = 5000
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
Secure the Config Files
sudo chmod 640 /etc/postfix/mysql-*.cf
sudo chgrp postfix /etc/postfix/mysql-*.cf
sudo systemctl reload postfix
Dovecot Integration for IMAP
# /etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:/var/mail/vhosts/%d/%n
mail_uid = vmail
mail_gid = vmail
mail_privileged_group = vmail
# /etc/dovecot/conf.d/auth-sql.conf.ext
passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
# /etc/dovecot/dovecot-sql.conf.ext
driver = mysql
connect = host=127.0.0.1 dbname=mailserver user=mailserver password=MailDBPass123
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM users WHERE email='%u' AND active = TRUE
user_query = SELECT 5000 AS uid, 5000 AS gid, CONCAT('/var/mail/vhosts/', maildir) AS home FROM users WHERE email='%u'
Adding a New Domain
# File-based: add domain to virtual_mailbox_domains in main.cf
# Database-based:
INSERT INTO domains (name) VALUES ('newdomain.com');
# Add DNS records for the new domain:
# MX record pointing to your mail server
# SPF, DKIM, DMARC records
# Postfix picks up database changes automatically (no reload needed)
Quota Management
# In Dovecot, enable quota plugin
# /etc/dovecot/conf.d/90-quota.conf
plugin {
quota = maildir:User quota
quota_rule = *:storage=1G
quota_rule2 = Trash:storage=+100M
}
# Per-user quota from database
# Add to user_query:
# quota_rule=*:bytes=%{quota} as quota_rule
Best Practices
- Use database-backed virtual domains for deployments with more than 5 domains or frequent changes
- Always use hashed passwords (SHA512-CRYPT or bcrypt) in the users table
- Create a dedicated database user with SELECT-only permissions for Postfix lookups
- Implement per-user quotas to prevent any single user from filling the disk
- Use Maildir format (not mbox) for better performance and reliability
- Set up proper DNS records (MX, SPF, DKIM, DMARC) for each virtual domain
- Monitor disk usage on the virtual mailbox base directory