Setup Email Server From Scratch Debian #2 - 05 PostfixAdmin
We believe in data independence, and support others who want data independence.
Debian Email From Scratch version 2 finished 2025-07-30.
We are still adding to it but it all works!
#################
# Postfix Admin #
#################
# PostfixAdmin requires a database but we installed this already on page 02 where
# we installed LAMP, Linux Apache MySQL PHP.
# Install PostfixAdmin
apt install postfixadmin
Use db-common config and enter a <supersecret> password, twice.
Don't forget your <supersecret> postfixadmin password.
nano /etc/apache2/sites-available/ssl-mx.okdeb.conf
# ---
Alias /admin-login /usr/share/postfixadmin/public
<Directory /usr/share/postfixadmin/>
Options FollowSymLinks MultiViews
AllowOverride All
Require all denied
Require ip trusted_network/24 trusted_ip1 trusted_ip2 trusted_ipv6
</Directory>
# ---
apachectl restart
# Setup access control
apt install acl
setfacl -R -m u:www-data:rwx /usr/share/postfixadmin/templates_c/
setfacl -R -m u:www-data:rx /etc/letsencrypt/live/ /etc/letsencrypt/archive/
# Configure PostfixAdmin
# Use one of these configs, not both ...
# more secure and efficient
$CONF['database_host'] = 'localhost';
$CONF['database_socket'] = '/run/mysqld/mysqld.sock';
# less secure less efficient
$CONF['database_host'] = 'localhost:3306';
$CONF['database_socket'] = '';
nano /usr/share/postfixadmin/config.local.php
# ---
<?php
$CONF['configured'] = true;
$CONF['database_type'] = 'mysqli';
// $CONF['database_host'] = 'localhost:3306';
// $CONF['database_socket'] = '';
$CONF['database_host'] = 'localhost';
$CONF['database_socket'] = '/run/mysqld/mysqld.sock';
$CONF['database_port'] = '3306';
$CONF['database_user'] = 'postfixadmin';
$CONF['database_password'] = 'supersecret';
$CONF['database_name'] = 'postfixadmin';
$CONF['encrypt'] = 'dovecot:ARGON2I';
$CONF['dovecotpw'] = "/usr/bin/doveadm pw -r 5";
if(@file_exists('/usr/bin/doveadm')) { // @ to silence openbase_dir stuff; see https://github.com/postfixadmin/postfixadmin/issues/171
$CONF['dovecotpw'] = "/usr/bin/doveadm pw -r 5"; # debian
}
//$CONF['setup_password'] = '';
$CONF['base_url'] = '/admin-login/';
$CONF['quota'] = 'YES';
$CONF['used_quotas'] = 'YES';
//allow maxmium 100 mailboxes for each domain
$CONF['mailboxes'] = '100';
//default 10240MB quota for each user
$CONF['maxquota'] = '10240';
//default 102400MB quota for each domain
$CONF['domain_quota_default'] = '102400';
?>
# ---
# Configure stats-reader and stats-writer sockets.
nano /etc/dovecot/conf.d/10-master.conf
# ---
service stats {
unix_listener stats-reader {
user = www-data
group = www-data
mode = 0660
}
unix_listener stats-writer {
user = www-data
group = www-data
mode = 0660
}
}
# ---
# add www to group dovecot
adduser www-data dovecot
systemctl restart postfix dovecot
root@okdeb.com:/usr/share/postfixadmin# ls -ld /var/run/dovecot/stats-*
srw-rw---- 1 www-data www-data 0 Jul 16 15:18 /var/run/dovecot/stats-reader
srw-rw---- 1 www-data www-data 0 Jul 16 15:18 /var/run/dovecot/stats-writer
# If permissions are not correct use ...
# chown www-data:www-data /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer
# chmod 660 /var/run/dovecot/stats-reader /var/run/dovecot/stats-writer
# Navigate to the setup web address.
https://mx.okdeb.com/admin-login/setup.php
# Enter a password and generate setup password hash and paste into config.local.conf.
nano /usr/share/postfixadmin/config.local.php
# ---
<?php
$CONF['configured'] = true;
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_socket'] = '/run/mysqld/mysqld.sock';
$CONF['database_port'] = '3306';
$CONF['database_user'] = 'postfixadmin';
$CONF['database_password'] = 'supersecret';
$CONF['database_name'] = 'postfixadmin';
$CONF['encrypt'] = 'dovecot:ARGON2I';
$CONF['dovecotpw'] = "/usr/bin/doveadm pw -r 5";
if(@file_exists('/usr/bin/doveadm')) { // @ to silence openbase_dir stuff; see https://>
$CONF['dovecotpw'] = "/usr/bin/doveadm pw -r 5"; # debian
}
$CONF['setup_password'] = '$2y$10$fds_SAMPLE_HASH_ONLY_ddlfdfudedEFDFGdsnjalksd';
$CONF['base_url'] = '/admin-login/';
$CONF['quota'] = 'YES';
$CONF['used_quotas'] = 'YES';
//allow maxmium 100 mailboxes for each domain
$CONF['mailboxes'] = '100';
//default 10240MB quota for each user
$CONF['maxquota'] = '10240';
//default 102400MB quota for each domain
$CONF['domain_quota_default'] = '102400';
?>
# ---
# Login with setup the password on the same page. Create an super admin account
# using a valid email address like adminuser@okdeb.com. A unix account isn't
# needed but to add one use ...
# Reload the setup page
https://mx.okdeb.com/admin-login/setup.php
# Generate Administrator accounts.
postmaster@okdeb.com
<password>
<password>
# Add postmaster@okdeb.com alias to aliases, redirect to user@okdeb.com for now...
nano /etc/mail/aliases
# ---
example_admin: user
# ---
newaliases
# Login
https://mx.okdeb.com/postfixadmin
postmaster@okdeb.com
<password>
# Configure Postfix to use MariaDB
# Add to end of file main.cf
nano /etc/postfix/main.cf
# ---
mailbox_transport = lmtp:unix:private/dovecot-lmtp
smtputf8_enable = no
virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/mysql_virtual_domains_maps.cf
virtual_mailbox_maps =
proxy:mysql:/etc/postfix/sql/mysql_virtual_mailbox_maps.cf,
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf
virtual_alias_maps =
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_maps.cf,
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_maps.cf,
proxy:mysql:/etc/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf
virtual_transport = lmtp:unix:private/dovecot-lmtp
# ---
mkdir /etc/postfix/sql/
nano /etc/postfix/sql/mysql_virtual_domains_maps.cf
# ---
user = postfixadmin
password = password
hosts = localhost
dbname = postfixadmin
query = SELECT domain FROM domain WHERE domain='%s' AND active = '1'
#query = SELECT domain FROM domain WHERE domain='%s'
#optional query to use when relaying for backup MX
#query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '0' AND active = '1'
#expansion_limit = 100
# ---
nano /etc/postfix/sql/mysql_virtual_mailbox_maps.cf
# ---
user = postfixadmin
password = password
hosts = localhost
dbname = postfixadmin
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1'
#expansion_limit = 100
# ---
nano /etc/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf
# ---
user = postfixadmin
password = password
hosts = localhost
dbname = postfixadmin
query = SELECT maildir FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' and mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) AND mailbox.active = 1 AND alias_domain.active='1'
# ---
nano /etc/postfix/sql/mysql_virtual_alias_maps.cf
# ---
user = postfixadmin
password = password
hosts = localhost
dbname = postfixadmin
query = SELECT goto FROM alias WHERE address='%s' AND active = '1'
#expansion_limit = 100
# ---
nano /etc/postfix/sql/mysql_virtual_alias_domain_maps.cf
# ---
user = postfixadmin
password = password
hosts = localhost
dbname = postfixadmin
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('%u', '@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'
# ---
nano /etc/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf
# ---
# handles catch-all settings of target-domain
user = postfixadmin
password = password
hosts = localhost
dbname = postfixadmin
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'
chown -R root:postfix /etc/postfix/sql
chmod 0640 /etc/postfix/sql/*
chmod 0750 /etc/postfix/sql
# Change postfix destination
postconf mydestination
mydestination = $mydomain, $myhostname, localhost.$mydomain, localhost
postconf -e "mydestination = okdeb.com, \$myhostname, localhost.\$mydomain, localhost"
# Add to end of file
nano /etc/postfix/main.cf
# ---
virtual_mailbox_base = /var/vmail
virtual_minimum_uid = 2000
virtual_uid_maps = static:2000
virtual_gid_maps = static:2000
# ---
adduser vmail --system --group --uid 2000 --disabled-login --no-create-home
mkdir /var/vmail/
chown -R vmail:vmail /var/vmail
systemctl restart postfix dovecot
# Configure Dovecot MySQL
# Make sure you have dovecot-mysql package installed
apt install dovecot-mysql dovecot-lmtpd
# Change the following
nano /etc/dovecot/conf.d/10-mail.conf
# ---
mail_location = maildir:~/Maildir
mail_home = /var/vmail/%d/%n
# ---
# Change the following, for mysql authentication use auth_username_format = %u
nano /etc/dovecot/conf.d/10-auth.conf
# ---
auth_username_format = %u
auth_default_realm = okdeb.com
#!include auth-system.conf.ext
!include auth-sql.conf.ext
auth_debug = yes
auth_debug_passwords = yes
# ---
# Change the following
nano /etc/dovecot/dovecot-sql.conf.ext
# ---
driver = mysql
connect = host=localhost dbname=postfixadmin user=postfixadmin password=super_secret_password
default_pass_scheme = ARGON2I
password_query = SELECT username AS user,password FROM mailbox WHERE username = '%u' AND active='1'
user_query = SELECT CONCAT('/var/vmail/', maildir) AS home, 2000 AS uid, 2000 AS gid, CONCAT('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active='1'
iterate_query = SELECT username AS user FROM mailbox
# ---
nano /etc/dovecot/20-lmtp.conf
# ---
protocol lmtp {
# Space separated list of plugins to load (default is global mail_plugins).
mail_plugins = $mail_plugins
}
# ---
nano /etc/dovecot/20-imap.conf
# ---
protocol imap {
# Space separated list of plugins to load (default is global mail_plugins).
mail_plugins = $mail_plugins
# Maximum number of IMAP connections allowed for a user from each IP address.
# NOTE: The username is compared case-sensitively.
#mail_max_userip_connections = 10
}
# ---
systemctl restart postfix dovecot
# If you have trouble restarting and get an error like module not found, unknown,
# not loaded make sure dovecot-mysql is installed with apt get dovecot-mysql.
# Thunderbird jack@okdeb.com will get disconnected because dovecot will be using
# the new virtual mailbox database and jack@okdeb.com doesn't exist there.
# Go to PostfixAdmin -> Domain List -> Add domain and mailboxes
Add Domain
Domain: okdeb.com
Description: my first mail domain
Aliases: 0
Mailboxes: 0
Mailbox Quota: 0
Domain Quota: 0
Active: x
Add default mail aliaes: x
Pass expires: 3650
# Go to Virtual List
# You may choose to add jack@okdeb.com back again, but it is good to add an
# admin email address for the default aliases, including postmaster@okdeb.com.
Add Mail Account jack@okdeb.com
Username: admin
****
****
Name: OkDeb Admin
Quota: 0
Active: x
Send Welcome email:
Add Mailbox
# Virtual List -> Show All - fix the aliases - by RFC keep postmaster@okdeb.com at least
abuse@okdeb.com jack@okdeb.com
** more ** jack@okdeb.com
postmaster@okdeb.com jack@okdeb.com
** more ** jack@okdeb.com
# Add New Alias
root@okdeb.com jack@okdeb.com
# Add or repair jack@okdeb.com to Thunderbird and test send and recieve to the account
# created entirely by PostfixAdmin
##############
# SUGGESTION #
##############
# If everything works as expected it is a good idea to backup the configurations
# now. We've done a lot of changes and made a lot of progress so let's make sure
# if we have problems later we can roll back to the previous working configurations.
cd /etc
tar cfzv postfix_backup.tgz postfix
tar cfzv dovecot_backup.tgz dovecot
tar cfzv apache24_backup.tgz apache2
cd /usr/share
tar cfzv postfixadmin_backup.tgz postfixadmin
cd /var/www
tar cfzv okdeb_backup.tgz okdeb
# If you have other users on your system make these tgz files unreadable and/or
# move them to a safe directory.
chmod 600 <filename.tgz>
# Next Up SPF DMARC and DKIM