Run your own mail server with Postfix and Dovecot

Ansible role with commentary for setting up your own mail server with Postfix and Dovecot.

This could be considered a part two of Mail relay, MX backup and spam filtering with Postfix. Many postfix configurations are identical between these setups.


Anyone can set up there own mail server and start exchanging e-mail with every other Internet user in the world. This is quite amazing I think.

So many things on the Internet today is controlled by a handful of tech giants. E-mail is something you can and should control yourself. It’s a bit complex to setup but done right it’s stable and low maintenance.

I have run my own mail servers for well over a decade. The setup I describe below has with minor changes been running in production since 2013. I recently upgraded them to Debian 9 Stretch and in early 2016 I started using Letsencrypt certs.

I host domains for my company and my family so mail between us are reasonably secure since all traffic uses TLS. E-mail is not a secure way to communicate but with your own server your mail is at least not used to target you for ads and what not.

To get started

Don’t be a cargo cult sysadmin, read the documentation.

Ansible mailserver role

Complete configurations can be found in my mailserver role at frjo/ansible-roles on Github. This is what I use to set up my own servers.

The common role that set up a firewall and other essentials on all my servers, the letsencrypt role for free certs and the dbserver role are also on GitHub. At the moment you will need to setup a web server with PHP support yourself, or take a look at Running Drupal on Debian 9 with Apache 2.4, HTTP/2, event MPM and PHP-FPM (via socks and proxy).

What you will get

  • Mail server with (almost) only standard Debian 9 packages so easy to keep updated via apt.
  • Virtual domains, mailboxes and aliases stored in MariaDB (MySQL but better).
  • Postfix for SMTP with opportunistic TLS, SPF and Postscreen configured.
  • Dovecot for IMAP/POP with required TLS.
  • PostfixAdmin - web based administration interface for Postfix mail servers. (the only non Debian package)
  • Spam filtering with DNSBL Spamhaus ZEN and BarracudaCentral.
  • Support for address extensions, addresses.
  • Striping of outgoing mail headers that reveal unneeded information like users IP address, mail client etc.

What you will not get

  • Webmail, see no use for that now that most people have a smartphone with a e-mail client built in.
  • Bayesian filtering or other text filtering systems. I believe this belongs on the client side.
  • DKIM/DMARK, I find them cumbersome and of no benefit.

DNS - get this right and good things will follow

Make sure the servers IP address is not blacklisted. It need to be a static address in good standing or your mail will get marked as junk.

The DNS record should look something like this. Please do not forget to set a valid PTF (pointer) record. In best case it should be the reverse of the A record but in must exist and be a valid address for the server.	3600	IN	A	3600	IN	PTR

With the MX record you tell all other mail servers what server handle the mail for your domain.		3600	IN	MX	10

If you set up some mail relay servers as I have done your MX record might look like this.		3600	IN	MX	10		3600	IN	MX	20

The SPF record tells other mail servers what servers are allowed to send mail for your domain.

The following is what I often use. It says that servers with a A or MX record for the domain is valid but none other. If you are using other external services to send e.g. news letters you need to add them as well.		3600	IN	TXT	"v=spf1 mx a -all"

If you run your web server on a separate host and have the MX records pointed at mail relays you can explicitly add the mail servers A record like this.		3600	IN	TXT	"v=spf1 a mx -all"

If you like some of my customers use Mailgun (or other services) to send out mail remember to add them to your SPF record. For Mailgun it looks like this.		3600	IN	TXT	"v=spf1 a mx -all"


Postfix handles the sending and receiving of mail. Dovecot is what users, or rather their mail client of choice, connect to when they want to read the mail.

Dovecot is the most standard compliant IMAP server and it just works. I used Courier for the first few years but never looked back after switching to Dovecot.

It supports the IDLE command so mail will arrive almost instantaneously on desktop mail clients that support it. Push support for iOS would be really nice to have and it’s possible to get working but it’s not straight forward.

Dovecot configuration consist mostly of making it talk with Postfix and MariaDB for user authentication. Postfix will handle all authentication via Dovecot.


A lot of the Postfix configuration is identical to the Mail Relay setup, see article link above. What follows is an quick overview of some Postfix configurations specific for the mail server.

The Ansible role will set up all this but it’s good to understand what it does and why.

Let Postfix in chroot jail access MariaDB sock

On Debian Postfix is by default set up to run in a chroot environment. This provides a significant barrier against intrusion. It also creates a stumbling block since it stops Postfix from accessing files outside the chroot jail.

By mounting /var/run/mysqld in /var/spool/postfix/var/run/mysqld we allow the postfix processes to access the MariaDB sock and all is well.

Authenticate via Dovecot

This will make Postfix authenticate users via Dovecot. Users mail clients will connect directly to Postfix for sending mail and we need them to authenticate.

# SASL settings
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_authenticated_header = yes

Virtual domains, mailboxes and aliases stored in MariaDB

This makes it possible to host multiple domains on one single mail server. Postfix will look up mailboxes and aliases in the database and deliver valid mail to dovecot or send onward if an alias point to an external address.

All local mail will be stored in the /var/spool/vmail directory belonging to the vmail user.

virtual_minimum_uid = 5000
virtual_gid_maps = static:5000
virtual_uid_maps = static:5000
virtual_mailbox_base = /var/spool/vmail
virtual_alias_domains =
virtual_alias_maps = proxy:mysql:/etc/postfix/
virtual_mailbox_domains = proxy:mysql:/etc/postfix/
virtual_mailbox_maps = proxy:mysql:/etc/postfix/
virtual_transport = dovecot
dovecot_destination_recipient_limit = 1
default_destination_concurrency_limit = 5
relay_destination_concurrency_limit = 1

Administer with PostfixAdmin

PostfixAdmin is a web interface written in PHP. It’s simple but works well for administering mailboxes and aliases for multiple domains.

The playbook is not using the latest version since I had trouble getting that to work. Since the older version works without issues I have not put a lot of time investigating the problem.

To be on the safe side I put it behind an Apache basic auth protection.

Removing headers on outgoing mail for security reasons

With smtp_header_checks you can manipulate the headers of mail sent out from the server by users. I use it to remove some headers for security reasons.

I remove X-Originating-IP, information about the mail client and the received header. This way the e-mail will appear to originate from the mail server itself and not reveal unnecessary information about the sending device/user.

# /etc/postfix/smtp_header_checks
# /^HEADER:.*content_to_act_on/ ACTION [MESSAGE]

/^Received:/                 IGNORE
/^User-Agent:/               IGNORE
/^X-Mailer:/                 IGNORE
/^X-Originating-IP:/         IGNORE

Further reading

Ars Technica has a four part article series “Taking e-mail back” that has a lot of good information.

Other good articles.

Mail relay, MX backup and spam filtering with Postfix

If you run your own mail server it is a good idea to have a MX backup in place. When your mail server goes down or you need to upgrade it, the MX backup will step in and store all mail until the mail server is back up.

A pure MX backup server is bit of a waste so I set mine up to act as a mail relay with spam filtering. Having two of them in separate locations seems like a good idea.

The setup I describe below has with minor changes been running in production since 2014. I recently upgraded them to Debian 9 Stretch and in early 2016 I started using Letsencrypt certs.

The servers handle mail for my own domains, for our company and for a number of customers. In total they deliver a bit over a thousand mail and discarded tens of thousands of spam per day. They do this with very little server resources.

Ansible mailrelay role

Complete configurations can be found in my mailrelay role at frjo/ansible-roles on Github. This is what I use to set up my own servers.

The common role that set up a firewall and other essentials on all my servers and the letsencrypt role for free certs are also on GitHub. If you use them plus the mailrelay role on a Debian 9 Stretch host you will get servers identical to mine.


I have been using Postfix for more than ten years. It might not be the easiest software to set up but mail in general is a bit complex. Postfix has proven to be extremely stable and reliable, actively maintained, well documented and with a good set of features.

The only separate software used in this setup is pypolicyd-spf, a Postfix policy engine for SPF. I have used it for years and it is as stable and good as Postfix itself.

Two external DNSBL services are used as well.

This makes this setup plain and simple, and very stable. Besides upgrading packages when needed they require almost no maintenance.

Relaying Mail and MX backup

First make sure the server is not an open relay, it would allow anyone sending mail through the server.

smtpd_relay_restrictions =

The “reject_unauth_destination” is the vital part.

The following tells postfix what mail to relay and where.

relay_domains = hash:/etc/postfix/transport
transport_maps = hash:/etc/postfix/transport
relay_recipient_maps = hash:/etc/postfix/relay_recipients

In the “transport” file set up each domain and where it should be relayed. This file is also used for the “relay_domains” parameter that will only read the first column.

# /etc/postfix/transport
# run  "postmap  /etc/postfix/transport"  after each edit

List all recipients that should be relayed in the “relay_recipients” file. Easiest is to simply list a domain and accept all addresses for it.

By instead specifying each real address the mail relay server can discard mail to non existing users directly. Then the list will however need to be updated when you add/remove addresses/mailboxes on your mail server.

# /etc/postfix/relay_recipients
# run  "postmap  /etc/postfix/relay_recipients"  after each edit        OK    OK     OK

Some other good settings include the maximum message size that I set to 25 MB, same as Gmail. The queue lifetime decides how long the server will keep trying to send mail. I set this to 10 days, this gives ample time to get a mail server up and running again.

message_size_limit = 25600000
maximal_queue_lifetime = 10d

See Postfix Configuration Parameters for a detailed explanation of all the parameters.

Spam filtering

With some basic spam filtering techniques around 98 percent of incoming mail are discarded with near zero false positives.

My biggest problem is with Swedish spam. For some inexplicable reason it is legal for a business to spam another business in Sweden. Mail services sending out what I consider spam is also sending out legit news letters etc. so one can not outright block them. It is a mess.

By just checking good DNSBL (DNS-based Blackhole List) 95 percent of incoming traffic can be discarded. I use Spamhaus ZEN and BarracudaCentral, they work really well and are solid services. Good DNSBL are by far the most important part of any spam filtering system.

Since I have paying customers on my servers I use the paid version of Spamhaus ZEN from Spamhaus Technology. Minimum cost is 250 USD for up to 355 mail users, a small cost for all the spam it stops.


Postscreen is a fast and light weight process included in Postfix 2.8 and later. It can filter out spam with the help of DNSBL before handing the rest over to the smtp server process. I highly recommend using it, it will keep server loads to a minimal.

# Postscreen
postscreen_greet_action = enforce
postscreen_dnsbl_action = enforce
postscreen_blacklist_action = enforce
postscreen_access_list = permit_mynetworks, cidr:/etc/postfix/client_access.cidr
postscreen_dnsbl_sites =,
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache

The client_access.cidr file can be used to whitelist (or blacklist) IP addresses. On my mail server I e.g. whitelist the IP addresses of the mail relay servers.

Filter bad attachments

With Postfix mime_header_checks you can easily reject mail with known bad file types. I e.g. block mails with bat|com|exe|jar|pif|scr|swf extensions, this is mostly to protect users with older versions of Windows.

# Filter on content in mime headers
mime_header_checks = pcre:/etc/postfix/mime_header_checks

Smtpd recipient restrictions

Most of the remaining spam gets discarded here. The two important parts are the SPF check and reject_unknown_reverse_client_hostname. Some guides suggest reject_unknown_client_hostname but I get to many false positive with that. There are a surprising number of mail operators that miss to set up proper PTR records. Do not make the same mistake for your own servers!

# Requirement for the recipient address.
smtpd_recipient_restrictions =
  check_client_access cidr:/etc/postfix/client_access.cidr,
  check_policy_service unix:private/policyd-spf,

See Postfix Configuration Parameters for a detailed explanation of all the parameters.

Bayesian filtering and other text filtering systems

I do not do this on any of my servers.

It complicates things and make false positives more likely. Better to do this on the client side and most mail clients today have good built in support for this. If you are on macOS I highly recommend SpamSieve.

MX records

With the servers up and running you need to reconfigure the DNS and make MX records for the new servers.

With this MX setup in the DNS:  MX  10  MX  20

Incoming mail flows like this:

Incoming mail ->              

If you are in Sweden and do not want to run your own mail relay and mx backup servers please have a look at my service Mail relay, MX backup och skräppostfilter. All the servers are in Sweden, one of the reasons I set them up to begin with.

A big commercial service that seems to have good reputation is Mail route. I have not used them myself. They have more advanced filtering and a web interface to handle settings, false positives etc.

Running Drupal on Debian 9 with Apache 2.4, HTTP/2, event MPM and PHP-FPM (via socks and proxy)

My article Running Drupal on Debian 8 with Apache 2.4, event MPM and PHP-FPM (via socks and proxy) is one of the most read on Here is the updated version for Debian 9.

I mention Drupal in the title but this setup should work well for most PHP based systems like Wordpress and Joomla etc.

It works equally well for static sites, like this one. Apache event MPM will handle all static files directly and the PHP part will never be used. This is one of the big benefits with event MPM over mod_php where every request have to drag PHP along.

Debian 9 comes with Apache 2.4.25, PHP 7 and MariaDB 10.1 so together with HTTP/2 the server should perform even better. Especially PHP 7 is a significant improvement for all PHP based apps.

There are only small changes needed to make this setup work for Debian 9 but I have also added information about HTTP/2 and the example vhost is TLS only. With free certs from Letsencrypt there is no reason not to use TLS, and many reasons to use it. Read my article Let’s Encrypt my servers with acme tiny for more.


Start by installing needed packages.

apt-get install apache2 apache2-dev php-fpm mariadb-server

You most likely want some more php extensions as well, here are the ones I normally install for running Drupal.

apt-get install php-cli php-apcu php-curl php-dev php-gd php-imagick php-json php-mysql php-mcrypt php-twig php-pear graphicsmagick graphicsmagick-imagemagick-compat

As suggested in PHP-FPM - Httpd Wiki I will run php-fpm via mod_proxy_fcgi so lets activate that module.

a2enmod proxy_fcgi

This will automatically activate the proxy module as well since it is a dependency. I also activate expires, headers, rewrite and ssl on my servers. Rewrite is needed for Drupal to get clean URLs.

Apache and HTTP/2 configuration

The Apache version that ships with Debian 9 supports HTTP/2 so lets activate it.

a2enmod http2

Restart Apache and add “Protocols h2 http/1.1” to your Apache conf. Best to add it per vhost as I have done in the example below.

That’s it, you are now serving pages over HTTP/2. There are interesting features like push to look in to but this is a good start.

Apache and PHP-FPM configurations

Debian by default sets up php-fpm to listen on a unix socket and since that should perform a bit better than a TCP socket I will use that. The most important setting is “max_children”. With Drupal each php process will typically use something like 20-40 MB. It can be a lot more for some sites so you simply need to test.

If your Drupal site use 30 MB per process setting “max_children” to 10 means that php will use up to about 10 * 30 MB = 300 MB of memory. A good resource for figuring out what is the best settings is this blog post Adjusting child processes for PHP-FPM (Nginx) · MYSHELL.CO.UK

listen = /run/php/php-fpm.sock
pm = dynamic
pm.max_children = 10
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 2000

The default mpm for Apache 2.4 (at least on Debian) is event mpm and since that is the most modern and best performing mpm there is no reason not to use it. I use the default settings and that should work well for most small servers. If needed I may up the value on ThreadsPerChild but I don’t think that will be needed on my servers.

# event MPM
# ServerLimit: upper limit on configurable number of processes (default = 16)
# StartServers: initial number of server processes to start (default = 3)
# MinSpareThreads: minimum number of worker threads which are kept spare (default = 25)
# MaxSpareThreads: maximum number of worker threads which are kept spare (default = 75)
# ThreadLimit: upper limit on the configurable number of threads per child process (default = 64)
# ThreadsPerChild: constant number of worker threads in each server process (default = 25)
# MaxRequestWorkers: maximum number of worker threads (default = ServerLimit x ThreadsPerChild)
# MaxConnectionsPerChild: maximum number of requests a server process serves (default = 0)
<IfModule mpm_event_module>
  ServerLimit             16
  StartServers            3
  MinSpareThreads         25
  MaxSpareThreads         75
  ThreadLimit             64
  ThreadsPerChild         25
  MaxConnectionsPerChild  2000

Apache vhost setup

Here we then come to the part that caused me the biggest problem. How to get php-fpm to only run the php files I wanted and not everything. The Apache wiki page above suggest using ProxyPassMatch but it turns out that will override any restrictions set in e.g. a Files/FilesMatch directive. For Drupal I want to block access to files like update.php and cron.php so another solution was needed.

I found the solution in a post from Mattias Geniar Apache 2.4: ProxyPass (For PHP) Taking Precedence Over Files/FilesMatch In Htaccess. His suggestion to use a SetHandle in a FileMatch directive seems to work very well.

Now with Debian 9 I see that this is the standard solution in /etc/apache2/conf-available/php7.0-fpm.conf that can be optionally activated. I prefer to do it for each vhost instead of globally. This also makes it easy to set different FPM pools for vhosts.

This is how I set up a vhost for serving Drupal.

<VirtualHost *:80>
  Redirect permanent /

<VirtualHost *:443>
  <IfModule mod_http2.c>
    Protocols h2 http/1.1

  <IfModule mod_ssl.c>
    SSLEngine on
    SSLCertificateFile /path/to/signed.crt
    SSLCertificateKeyFile /path/to/domain.key
    SSLCertificateChainFile /path/to/chain.pem
    <IfModule mod_headers.c>
      Header always set Strict-Transport-Security: "max-age=15768000"

  DocumentRoot /var/www/customers/example/web
  ErrorLog /var/www/customers/example/logs/error_log
  CustomLog /var/www/customers/example/logs/access_log combined
  <Directory "/var/www/customers/example/web">
    Options FollowSymLinks
    AllowOverride None
    Include /var/www/customers/example/web/.htaccess
    <IfModule mod_proxy_fcgi.c>
      # Run php-fpm via proxy_fcgi
      <FilesMatch \.php$>
        SetHandler "proxy:unix:/run/php/php-fpm.sock|fcgi://localhost"
    # Only allow access to cron.php etc. from localhost
    <FilesMatch "^(cron|install|update|xmlrpc)\.php">
      Require local

Notice that I include the .htaccess file. I have set “AllowOverride None” to prevent Apache from looking for and automatically include any .htaccess files it finds. This improves performance a bit but one needs to remember to reload Apache when changes are made to the .htaccess file.

Apache security configurations

Here follow the security related settings I use for Apache. See the included links for more information.

# Security settings
# and
<IfModule mod_headers.c>
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "sameorigin"
  Header always set X-Xss-Protection "1; mode=block"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  RequestHeader unset Proxy early

# SSL settings, see
<IfModule mod_ssl.c>
  SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
  SSLHonorCipherOrder on
  SSLCompression off
  SSLUseStapling on
  SSLStaplingResponderTimeout 5
  SSLStaplingReturnResponderErrors off
  SSLStaplingCache shmcb:${APACHE_RUN_DIR}/ocsp_scache(128000)
  SSLSessionCache shmcb:${APACHE_RUN_DIR}/ssl_scache(512000)
  SSLSessionCacheTimeout 300

Extra security configurations in Apache for Drupal

Drupal put .htaccess in the files folder and some other places for security reasons. The following is an example how to add the same security configurations directly in an Apache conf file. The DirectoryMatch regex most likely needs adjustment for your directory structure.

At the top there are some settings to deny access to version control folders and some Drupal core text files.

# Prevent access to .bzr and .git directories and files.
<DirectoryMatch "/\.(bzr|git)">
  Require all denied

# Prevent access do some Drupal txt files.
  Require all denied

# Security setting for files folder in Drupal.
<DirectoryMatch "^/var/www/.*/sites/.*/(files|tmp)">
  # Turn off all options we don't need.
  Options -Indexes -ExecCGI -Includes -MultiViews

  # Set the catch-all handler to prevent scripts from being executed.
  SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
  <Files *>
    # Override the handler again if we're run later in the evaluation list.
    SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003

  # If we know how to do it safely, disable the PHP engine entirely.
  <IfModule mod_php.c>
    php_flag engine off

# Security setting for config folder in Drupal.
<DirectoryMatch "^/var/www/.*/sites/.*/(private|config|sync|translations|twig)">
  <IfModule mod_authz_core.c>
    Require all denied

  # Deny all requests from Apache 2.0-2.2.
  <IfModule !mod_authz_core.c>
    Deny from all
  # Turn off all options we don't need.
  Options -Indexes -ExecCGI -Includes -MultiViews

  # Set the catch-all handler to prevent scripts from being executed.
  SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
  <Files *>
    # Override the handler again if we're run later in the evaluation list.
    SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003

  # If we know how to do it safely, disable the PHP engine entirely.
  <IfModule mod_php.c>
    php_flag engine off

MariaDB instead of MySQL

I have completely switched from MySQL to MariaDB for all new deployments. Version 10+ of MariaDB is a noticeable performance improvement and has better defaults values for various settings. MariaDB is run by the people who originally created MySQL, before is was bought by Sun and then swallowed up by Oracle.

Below are what I put in /etc/mysql/conf.d/local.cnf. You will need to adjust at least the innodb_buffer_pool_size depending upon how much memory the server have and the size of InnoDB data and indexes. This answer on stack exchange has a lot of interesting information about this How large should be mysql innodb_buffer_pool_size?.

# Set character set and collation to utf8mb4.
character_set_server = utf8mb4
collation_server = utf8mb4_unicode_ci

# Common Configuration
skip_name_resolve = 1
connect_timeout = 10
interactive_timeout = 25
wait_timeout = 60
max_allowed_packet = 64M
table_open_cache = 2000
table_definition_cache =  2000
thread_handling = pool-of-threads

# Slow Log Configuration
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql_slow_query.log
long_query_time = 5
#log_queries_not_using_indexes = 1

# InnoDB Configuration
innodb_buffer_pool_size = 256M
innodb_flush_method = O_DIRECT
innodb_file_per_table = 1
innodb_flush_log_at_trx_commit = 2
innodb_large_prefix = 1
innodb_file_format = barracuda

Blog archive