A Dockerized Self Hosted Mail Server With Postfix And Dovecot
Wether you want to have control over your own mails, not share data with an unknown 3rd party, not pay for several accounts when you could just pay for one server or you just wanna waste some time with another project: there are a lot of reasons to run your own mail server. But actually setting up a mail server that works can be tricky.
In this article we explore how to set up a self hosted mail server that can serve multiple mail domains on the internet. We run all applications like the required mail transfer agent (MTA) and IMAP server on one machine. The users we want to receive mails for and send mails from don’t have an account on that machine. Rather, we store their user data in a database that the other applications can access. We run the mail server processes in docker containers to make the setup more modular and get rid of the dependency to the underlying operating system and its library versions.
I already show how to create a docker image for the mail transfer agent (MTA) Postfix and IMAP Server Dovecot in other posts. We will not go through that information here as it would make this article too long and cluttered. The config settings in this article are geared towards these docker images but you can of course adapt them to fit other installations if you want to. For the sake of brevity there is no section on spam filtering or mail frontends. We will pick up these topics in future posts.
Also: This posts mostly focuses on the configuration aspect and less on the technical details of a mail server and the protocols it uses.
A word of advice: for your mail server to accept mail from other domains you have to expose it TO THE INTERNET. If you now think: well, that sounds like a dumb idea - it probably is. So take running a mail server seriously, do your updates and learn how to secure your server and data. Or to put it another way:
With that said: let’s create our mail server!
Preparations
In addition to setting up the necessary docker containers on our server we have to point our domain(s) to the mail server.
We assume that you have a working server with docker installed and can access it with Ansible. If you need help setting up Ansible, check out the first paragraphs of how to deploy and configure ApacheDS. You will later need the LDAP server anyway.
Domain Setup
For our mail server to be able to receive mails from other mail servers on the internet we need to make it known to anyone looking for it. Other mail servers will infer where mails for us should be sent to by looking at the domain part of the To
mail address (the part behind the @
) in the mail to be sent.
Let’s say our domain is mydomain.com
. The other mail servers will look for a MX DNS record in your domain DNS entry. That’s where one normally expects a reference to the mail server for your domain. If they don’t find a MX record, they will try the A record of your domain.
So, if you want to run your mail server on the machine your domain A record points to you don’t need to do anything. But if you want to use another machine as mail server or have a spare mail server then you need to set a MX record.
Example:
# Domain |
So for your domain mydomain.com
mails should go to mx.mydomain.com
which in turn is the domain name of your mail server.
You have to set the records in the UI of your domain name registrar. There are countless offerings available on the internet, so it is hard to recommend a specific service. Maybe the one where you bought your domain?
Ansible and Docker Setup
In addition to the general Ansible setup we use an internal network bridge to connect the user database, MTA and IMAP server. Here is an Ansible task to create said network:
- name: Ensure docker internal network |
Also, we assume that you have your certificates at:
/etc/ssl/certs/mx.mydomain.com.crt |
Now you can use the snakeoil certificate that most systems have as default but you should really get a proper one either through Let’s Encrypt or another certification authority.
User database
Our mail server needs to know for which addresses it shall send and receive mails. There are a number of ways to configure this – most importantly: a list of users which we store in the LDAP server ApacheDS. There is already a tutorial on how to deploy and configure ApacheDS including a docker repository for ApacheDS. But here is a quick ansible task that starts ApacheDS and also connects it to our internal
bridge:
|
We use a new partition in ApacheDS for the mail server data. You can add a new partition via the server configuration (right click on your connection -> open configuration) in the partitions tab. After adding the partition you have to restart ApacheDS or the partition won’t show up.
The child entry people
of type organizationalUnit
in the new partition then contains the mail server users. The mail server user entries have type inetOrgPerson
.
Our schema:
# Partition |
The user entries should have a SSHA256 userPassword
attribute. You can pick a different schema but for the remainder of this article we assume you use the same schema as shown above.
IMAP Server
Now we know where to find the users we want to send and receive mails for. Let’s think about where to store their mails. Our software of choice is the IMAP server Dovecot. The following config is written for the dovecot docker image recipe but you can adapt it to your docker image of choice or even a bare metal installation.
There are a number of ways to organize the dovecot config file(s). You can map the following config snippets as a single dovecot config file /etc/dovecot/dovecot.conf
to the config diretory.
Dovecot Protocol and Service Configuration
First we have to tell Dovecot which protocols to support. Obviously we want IMAP so that the user can access their mails from a mail client. We will also enable LMTP. This is the protocol we use to locally deliver mails to Dovecot.
protocols = imap lmtp |
We want Dovecot to listen for LMTP connections on port 24:
service lmtp { |
But LMTP can do more: we also can ask Dovecot through LMTP wether a user exists in Dovecot’s user database. We will use this feature later in Postfix. For now, we want Dovecot to listen for auth requests on port 10010:
service auth { |
Another basic setting: when Dovecot has to send mail use this address:
postmaster_address = postmaster@mydomain.com |
Dovecot User Config
Dovecot has to know which users shall be allowed to access the mail storage. Dovecot splits user data from password data but we will just refer to LDAP in both cases:
passdb { |
We put the LDAP connection information into a separate file which we discuss in the next section.
Dovecot LDAP Config
The LDAP config has to tell Dovecot where to find the LDAP server and how to access it as well as where to find the user and password data in the LDAP tree.
The following config is tailored to the LDAP schema we create above.
# Use your LDAP server address |
Dovecot Auth Config
Ok, but users still have to log in. We allow the plain and login auth mechanisms. We also set the default realm to our domain so the user can just use their username instead of their full mail address when connecting to the Dovecot. You might not want to allow this when you serve multiple domains but it’s handy when you only serve one domain.
auth_mechanisms = plain login |
Dovecot Logging Config
If you have problems you can always turn on logging. Set the following switches to yes
to get more verbose output.
We run Dovecot in a docker container and therefore direct the log output to /dev/stderr
so that it will appear in the docker logs instead in a file in the container.
# What to log verbose |
Configure Mail Storage in Dovecot
Dovecot can store mails in various formats. We choose Maildir because of its simplicity. With Maildir every mail is a separate file on the harddrive. Hopefully that will make recovery easier in the unavoidable event of total desaster. But depending on your workload you might choose another of their mailbox formats.
We define a home directory path for each user that comprises the mail domain of the user and the username. The home directory will hold all mail files for that user plus potential script and other files. Then we set the mail location to a Maildir
folder in the users home directory. The mail_location
string also encodes the mailbox format. In this case: Maildir.
mail_home = /var/spool/mail/vhosts/%d/%n |
We also tell Dovecot to use the mail folder inbox
as inbox.
namespace inbox { |
If you run Dovecot on a different user or group from the default you have to tell Dovecot about it so it can access the mail directories properly. The following values work for the dovecot docker image.
mail_uid = dovecot |
If you want Dovecot to compress the mail files before saving them to disk, enable the zlib plugin:
# Use the $mail_plugins variable to add to the existing mail_plugins value |
Dovecot SSL Options
Users should not connect to Dovecot using insecure connections as that exposes their mails and passwords. To secure the connection we configure Dovecot to use SSL.
The SSL configuration is straight forward. You just turn it on and tell Dovecot which certificate to use.
ssl = yes |
Dovecot Default Directories
Ok, let’s finally mark some more folders as special so that Dovecot and mail clients know where to put drafts, junk, trash and so on:
### Mailboxes |
Dovecot Setup With Ansible
This is an Ansible role to get Dovecot up and running using the Dovecot docker image. We mount the directory containing the mail and config files to directories on the host system. You might wanna opt for Docker volumes if necessary.
|
Postfix
A mail transfer agent (MTA) receives mails from the internet or sends mails from our users to their destination. The MTA is exposed to the internet and should be configured very carefully to not become an accidental spam relay or be abused otherwise. Users that want to send mails through our MTA must authenticate. Incoming mail must be checked for various criterias before being accepted. There are a lot of MTA implementations. We will use Postfix for this post.
The following config is written for the Postfix docker image recipe.
Enable Postfix services
For most capabilities Postfix has it will run a service. We can control what services Postfix will run through its /etc/postfix/master.cf config file.
Let’s check that our most important services are configured. For incoming mails from other MTAs we need to enable the smtp service:
smtp inet n - n - - smtpd |
And for mails that our users want to send we use the submission service. Here we force SSL through an extra option to make sure the user authenticates securely.
submission inet n - n - - smtpd |
If you are using the Postfix docker image the rest of the master.cf
is fine.
Domain Setup
The rest of the config settings go into /etc/postfix/main.cf
. First we tell Postfix what Domain it is running from and what its surrounding network looks like.
mydomain = mydomain.com |
And because we want to serve mails for potentially multiple domains, we restrict normal incoming mails to the localhost:
# 172.200.0.0/16 is anything from our docker bridge |
Also we don’t want to relay mails for other MTAs. So any mail that arrived via SMTP either goes to Dovecot or is rejected.
relay_domains = |
If we send mail we want it to appear to come from here:
myorigin = $mydomain |
And if someone connects to us, greet them with:
smtpd_banner = $myhostname ESMTP |
Virtual Domain(s)
All domains that we actually serve will be virtual domains. That means that our server will accept mails for these domains although it might not be its own domain. We pass a list of all domains we want to serve and that we want to send them to dovecot.
virtual_mailbox_domains = $mydomain |
If we have any virtual users that have alias addresses, we can specify a file for that:
virtual_alias_maps = lmdb:/etc/postfix/virtual |
That file will look like this:
admin@mydomain.com postmaster@mydomain.com |
Which means all mails to admin@mydomain.com
will be delivered to postmaster@mydomain.com
User Authentication
To prevent our MTA from becoming a spam relay we require users that want to send mail through us to authenticate. We can provide Postfix with an LDAP connection config so that it takes the user information from ApacheDS like Dovecot does. But we also can direct Postfix to ask Dovecot wether a user exists or not through LMTP. That way we don’t have to replicate the LDAP connection config.
Also we force users to use TLS to connect to the submission port.
smtpd_sasl_type = dovecot |
We later enable the user check through dovecot using the reject_unverified_recipient
policy.
SSL
For everyone who wants to send mails to our users we enable opportunistic encryption: if they want it, they can use it. We also reuse the certificates we use for dovecot. It’s the same domain after all.
smtpd_tls_cert_file = /etc/postfix/my_cert.crt |
Logging
We are running postfix in a docker container so we want all output to go to stdout so docker logs can pick it up.
### logging and debugging: docker friendly logging and minimal error output |
Libraries
Postfix on Alpine 3.13 and later will no longer support BTree and Hash files. Instead, use lmdb files like in the Postfix docker image. But we have to enable the lmdb library in a special file first:
lmdb postfix-lmdb.so dict_lmdb_open mkmap_lmdb_open |
Security
Right now we havent’t really restricted anyone from sending mails through our mail server yet, so we will do that now. Postfix actually requires you so specify some policies that shall prevent it from beeing an open relay, which means that anyone could send mails from our server.
Any of these policies might work for you or accidentially restrict the wrong users or other MTAs from delivering mail. You should check your mail logs to see if there is any unexpected behaviour. For me the following configuration seems to work.
First, we require some basic stuff:
### Other MTAs must use a proper HELO as specified in RFC5321 |
Now for who is allowed to send mail. We want clients from our local network and our users to be able to send mail to the outside and to other users on our server. Other MTAs shall be able to send mails to users on our server.
Again: we use Dovecot to do the user checks for us using the reject_unverified_recipient
check.
Postfix will do the following checks in order for every mail it receives. It will perform the checks after it receives the recipient in the mail text. If you know SMTP you might think: why not do them earlier and use smtpd_helo_restrictions
or smtpd_sender_restrictions
? Well, postfix will receive the mail up to the recipient anyway for performance reasons, so we can as well check them with more information available.
smtpd_recipient_restrictions = |
We will also do some additional checks for the data part of the mail:
smtpd_data_restrictions = |
Note: reject_multi_recipient_bounce
might cause problems because there are legitimate use cases for null senders. But I never had these problems, so I continue to use it.
We reference some files in the smtpd_recipient_restrictions
above:
If a client presents with one of these identities, it’s spam:
/^mx\.mydomain\.com$/ 550 Don't use my hostname. |
Here we just list some IP ranges that cannot be valid MX servers, because they would be on our network or us:
# invalid sender IPs |
We accept some special addresses that other mail admins would use to contact us in case anything is wrong.
postmaster@ OK |
Postfix Deployment With Ansible
Now that we got our config, here is an Ansible role to deploy to Postfix as docker container to your server. Just keep the config files you didn’t touch. Reuse the SSL certificate from Dovecot.
|
Final Thoughts
Ok, that was a lot of config files. I said it before: running a mail server isn’t easy. But if you want to do it I hope I was able to provide a starting point.