Use Docker-MailServer to Build Self-hosted Mail Server
Advanced but detailed instruction for self-hosted mail server

1. Background
It’s kinda cool to have your mailbox with your own domain, such as [email protected]. Meanwhile, as long as you are the owner of your domain, the easiest approach is to have a third party mail server with a custom domain configuration. Most of the services charge a little bit, like Google Workspace, Amazon Workmail, etc. It doesn’t matter but more importantly, you don’t have the absolute privilege of the managed services.
Actually I tried to build and deploy a self-hosted mail server in 2019. Due to my very limited knowledge, it failed. With the timing of ChatGPT suddenly becoming popular, I rediscovered my enthusiasm for dev recently. So I wanna retry it and this time it was a one-shot success.
2. Introduction
From the name itself you can tell that this docker-mailserver is deployed with Docker. Docker-mailserver has no built-in webmail, which means it is a pure mail server and does not occupy port 80/443. This has a great advantage that we have both various other services and mail service on the same server at the same time.
There are three reasons why I recommend this mail server that can be deployed directly with Docker:
-
- Simple / Convenient. You have to know that manually configuring such a fully functional mail server may not be done in one night, and even a slight mistake may cause you tons of trouble.
-
- Secure. As a mail server, self-use is not a big deal. But if it is a public service, with all kinds of spam & virus mail, you will find that the original mail server is really difficult to maintain.
-
- This is the top reason for me personally. I think it is a waste to have a dedicated mail server. So using Docker like this, after the deployment is complete, I can also deploy other applications. Although I’m using Nginx as a web server and SNI function to re-use the port(s), Nginx still have to take charge of port 80/443. I will have no worries if I use docker-mailserver.
In this article, I will show you how to use Docker-MailServer to build your own self-hosted mail server.
- This is the top reason for me personally. I think it is a waste to have a dedicated mail server. So using Docker like this, after the deployment is complete, I can also deploy other applications. Although I’m using Nginx as a web server and SNI function to re-use the port(s), Nginx still have to take charge of port 80/443. I will have no worries if I use docker-mailserver.
3. System Basics
- Linux server, whether it’s AMD64 or ARM64
- Docker and docker-compose
curl -sSL https://get.docker.com/ | sh
systemctl enable docker
curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
Note: I’m still using docker-compose v1, 1.29.2 is the last version of v1. If you prefer to use docker-compose v2, please refer to docker-compose release page and carefully read the difference.
3. Firewall
In order to use mail service, you have to open the firewall of certain ports of your server.
Port | Type | Purpose |
---|---|---|
25 | TCP | explicit TLS => STARTTLS |
143 | TCP | explicit TLS => STARTTLS |
465 | TCP | implicit TLS |
587 | TCP | explicit TLS => STARTTLS |
993 | TCP | implicit TLS |
4. DNS
DNS resolve configuration, take CloudFlare as an example:
Type | Name | Content |
---|---|---|
A | <mail_server_ip> | |
A (optional) |
@ (optional) |
<app_server_ip> (optional, can be different from mail server ip) |
MX | @ | mail.example.com (random priority is fine. 10/20/50 is common) |
TXT | _dmarc | v=DMARC1; p=reject; rua=mailto:[email protected] |
TXT | mail._domainkey | v=DKIM1; h=sha256; k=rsa; p=????? (to be filled out later) |
TXT | @ | v=spf1 mx ~all |
Note1: If you are using CloudFlare, DO NOT turn on the proxied
for the first row, which is the line of Type = A / Name = mail / Content = <mail_server_ip>. The icon should be GREYED OUT.
Note2: From above, [email protected]
is to be changed to an email address that you can receive emails normally.
Note3: Content of mail._domainkey
will be filled out in later steps.
5. Prerequisites
5.1 Pull the image
docker pull docker.io/mailserver/docker-mailserver:latest
5.2 Get the script & env
mkdir mailserver && cd mailserver
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh
chmod a+x ./setup.sh
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/docker-compose.yml
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/mailserver.env
5.3 Check ports
Find out which applications currently occupy port 25/143/465/587/993 on the server
netstat -nltp
If Postfix is enabled by default, we should disable it:
systemctl stop postfix
systemctl disable postfix
6. Certificate with Certbot
In this article we will use Let’s Encrypt with certbot. Feel free to use other certificate with other authorities. However you cannot use CloudFlare Origin Certificates with the proxied status ON.
- Install certbot
apt install certbot
- Run certbot in test mode (to avoid temporary ban due to misconfiguration)
certbot certonly --dry-run
You will probably see below. Remember to select standalone
and enter the correct domain (with mail.)
Saving debug log to /var/log/letsencrypt/letsencrypt.log
How would you like to authenticate with the ACME CA?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Nginx Web Server plugin (nginx)
2: Spin up a temporary webserver (standalone)
3: Place files in webroot directory (webroot)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-3] then [enter] (press 'c' to cancel): 2
Please enter the domain name(s) you would like on your certificate (comma and/or
space separated) (Enter 'c' to cancel): mail.example.com
- If the result above is successful, run the command again without dry-run parameter
certbot certonly
- Location of you certificate
The certificate will be saved at/etc/letsencrypt/archive/mail.example.com/
Important: Here is the tricky part. Since the docker container will read the files in the local volume, and the Let’s Encrypt certs under “live” folder is a symbolic link, which means if you point the container to/etc/letsencrypt/live/mail.example.com/
, it cannot reach to the original certs themself. Even if you finish all the setting, some error message will pop up, like:
mailserver | [ ERROR ] TLS Setup [SSL_TYPE=manual] | File /tmp/ssl/privkey.pem or /tmp/ssl/fullchain.pem does not exist!
mailserver | [ ERROR ] Shutting down
To solve this, we can point the container to upper level, which includes the “archive” folder and create two new symbolic links:
cd /etc/letsencrypt
ln -s ./live/mail.moogpt.com/privkey.pem privkey.pem
ln -s ./live/mail.moogpt.com/fullchain.pem fullchain.pem
This step is to ensure the auto-renew of Let’s Encrypt.
7. Configuration
7.1 docker-compose.yml
nano docker-compose.yml
It’s recommended to add CloudFlare DNS into the yml dns: 1.1.1.1
The complete yml is like (example):
services:
mailserver:
image: docker.io/mailserver/docker-mailserver:latest
container_name: mailserver
# If the FQDN for your mail-server is only two labels (eg: example.com),
# you can assign this entirely to `hostname` and remove `domainname`.
hostname: mail.example.com # replace with your domain
domainname: example.com # replace with your domain
dns: 1.1.1.1 # This line is newly added and recommended
env_file: mailserver.env
# More information about the mail-server ports:
# https://docker-mailserver.github.io/docker-mailserver/latest/config/security/understanding-the-ports/
# To avoid conflicts with yaml base-60 float, DO NOT remove the quotation marks.
ports:
- "25:25" # SMTP (explicit TLS => STARTTLS)
- "143:143" # IMAP4 (explicit TLS => STARTTLS)
- "465:465" # ESMTP (implicit TLS)
- "587:587" # ESMTP (explicit TLS => STARTTLS)
- "993:993" # IMAP4 (implicit TLS)
volumes:
- ./docker-data/dms/mail-data/:/var/mail/
- ./docker-data/dms/mail-state/:/var/mail-state/
- ./docker-data/dms/mail-logs/:/var/log/mail/
- ./docker-data/dms/config/:/tmp/docker-mailserver/
- /etc/localtime:/etc/localtime:ro
- /etc/letsencrypt/:/tmp/ssl:ro # Add this line. It will be used later.
restart: always
stop_grace_period: 1m
cap_add:
- NET_ADMIN
healthcheck:
test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1"
timeout: 3s
retries: 0
7.2 mailserver.env
Within this env file, you must modify the following:
- PERMIT_DOCKER
# Set different options for mynetworks option (can be overwrite in postfix-main.cf)
# **WARNING**: Adding the docker network's gateway to the list of trusted hosts, e.g. using the `network` or
# `connected-networks` option, can create an open relay
# https://github.com/docker-mailserver/docker-mailserver/issues/1405#issuecomment-590106498
# The same can happen for rootless podman. To prevent this, set the value to "none" or configure slirp4netns
# https://github.com/docker-mailserver/docker-mailserver/issues/2377
#
# none => Explicitly force authentication
# container => Container IP address only
# host => Add docker container network (ipv4 only)
# network => Add all docker container networks (ipv4 only)
# connected-networks => Add all connected docker networks (ipv4 only)
PERMIT_DOCKER=network
- SSL_TYPE
# Please read [the SSL page in the documentation](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/ssl) for more information.
#
# empty => SSL disabled
# letsencrypt => Enables Let's Encrypt certificates
# custom => Enables custom certificates
# manual => Let's you manually specify locations of your SSL certificates for non-standard cases
# self-signed => Enables self-signed certificates
SSL_TYPE=manual
- SSL_CERT_PATH and SSL_KEY_PATH
# These are only supported with `SSL_TYPE=manual`.
# Provide the path to your cert and key files that you've mounted access to within the container.
SSL_CERT_PATH=/tmp/ssl/fullchain.pem
SSL_KEY_PATH=/tmp/ssl/privkey.pem
# Optional: A 2nd certificate can be supported as fallback (dual cert support), eg ECDSA with an RSA fallback.
# Useful for additional compatibility with older MTA and MUA (eg pre-2015).
SSL_ALT_CERT_PATH=
SSL_ALT_KEY_PATH=
Note: SSL_CERT_PATH
and SSL_KEY_PATH
are the paths within the container, not local volume.
8. Final Settings
- Create an email account. You can replace
admin
with whatever you like. Don’t forget to change the password.
cd /your/path/mailserver
./setup.sh email add [email protected] password
- Generate a DKIM key
./setup.sh config dkim
The resolve record of DKIM is saved in:
/your/path/mailserver/docker-data/dms/config/opendkim/keys/moogpt.com/mail.txt
- Fill out mail._domainkey in DNS (in Step 4)
The content of mail.txt is something like below (example, link the parts together):
mail._domainkey IN TXT v=DKIM1; h=sha256; k=rsa;
p=MIICIjANBgkfdsjanfjkds34rfrsnfjl.gKCAgEAme22c10H25DwMw90ob7RYozozQ2ONchrLGFWb8gulxxMcOpWo+tmAjFob6Hn/7u49nEW28W3EGfnisdfuvndafijlnvfCHuejKzEeE0LMOTpzRTDeCQkLlSKhzfsdanfj4irjfgvierfdnviofdfakRwtObe+L8HZeUZnBDXsCAh/lmgI3bRQQZkqcfRVquwh132hkgvjkhyBAJiKrevbuqTPHOKioWMFsg0EAj7UfenarufoidsvndfijonvjifYeRgZHC1s6jsZjBQig4gvyOCL0eP4fwi4eriognveourafisdfjvECRajAXgwSOaEjvV++CYImiF9T9tnc7FppcFyJ6KhYRaoF+NbTz8GYQ3gzTKh2hxmoeFSsa+YP7G/DD0Ru2yLkqKy8QCMyqTgJHpITD0JjmYMBxwjFrZRGKnp5Qo8Ssd/U5x0AGV698ePker08ghdsh80Mvmnfjonuvh9rjfg03oPxBwtz0/3ZGEQ94qRMN7bx93oynRD/XKBqgrK9R0LP1AGQxVkT+1aWVDhhCLQ9PL6Z9oqkeuXvcGcdUvGg4Ww5f8DowGcfadosufhBgQ9/WthcIwWCtVPgtIrXHuKTgWfjnfwX6vkert0igfsaeijrsdnfuionerU+M64Jst0oe5+FdTVNaOVPkCAwEAAQ==
9. Bring Up the Container
cd /your/path/mailserver
docker-compose up -d
10. Client Setting
For incoming mail server:
- Host Name: mail.example.com
- User Name: [email protected]
- Password: password
For outgoing mail server (SMTP, iPhone for example):
- Host Name: mail.example.com
- User Name: [email protected]
- Password: password
- Use SSL: on
- Authentication: Password
- Server Port: 465
Both incoming and outgoing function have been tested with the settings in this article.
Copyright statement: Unless otherwise stated, all articles on this blog adopt the CC BY-NC-SA 4.0 license agreement. For non-commercial reprints and citations, please indicate the author: Henry, and original article URL. For commercial reprints, please contact the author for authorization.