This blog post will show how to easily deploy a Mailu server using Docker (and docker-compose). The project is already providing Nginx as a (much needed) reverse-proxy but since I am a fan of Traefik, I will also give the configuration I used in case, like me, you have multiple services deployed on several hosts and are using Traefik to properly handle the HTTP/TCP/UDP routing (yes, Traefik does all of this!).
For the encryption part, we will use Letsencrypt to generate the certificate(s) for our domain(s).
Recommanded setup:
CPU: at least 1 CPU @ 2.0Ghz
RAM: at least 1GB of RAM (+1GB of swap)
Disk: enough disk space available to store your emails: for durability purposes you should use a FS backed by a RAID-1 device, or from a distributed FS like GlusterFS
You would need:
a domain (example.com
in this post), where email.example.com
will be used for Mailu (webmail, admin, SMTP, IMAP, POP3 - all in their secure and unsecure versions)
a host with Docker and docker-compose
installed
a host with a publicly accessible IPv4 (1.2.3.4
in this post)
In this post I will consider the base path to save any of Mailu data is /var/lib/mailu
. You are free to use anything else that would better suit your (HA) needs.
Mailu provides a web utility to generate the configurations needed. It can be used to generate the docker-compose.yml
and mailu.env
files. Below are the same files, adjusted for the example.com
example domain.
docker-compose.yml
---
version: '2.1'
services:
# External dependencies
redis:
networks:
- mail
image: redis:alpine
restart: always
volumes:
- "/var/lib/mailu/data/redis:/data"
# Core services
front:
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}nginx:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
networks:
- mail
logging:
driver: json-file
ports:
- "1.2.3.4:80:80"
- "1.2.3.4:443:443"
- "1.2.3.4:25:25"
- "1.2.3.4:465:465"
- "1.2.3.4:587:587"
- "1.2.3.4:110:110"
- "1.2.3.4:995:995"
- "1.2.3.4:143:143"
- "1.2.3.4:993:993"
volumes:
- "/var/lib/mailu/certs:/certs"
# - "/var/lib/mailu/overrides/nginx:/overrides"
admin:
networks:
- mail
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}admin:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
volumes:
- "/var/lib/mailu/data:/data"
- "/var/lib/mailu/data/dkim:/dkim"
depends_on:
- redis
imap:
networks:
- mail
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}dovecot:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
volumes:
- "/var/lib/mailu/data/mail:/mail"
# - "/var/lib/mailu/overrides:/overrides"
depends_on:
- front
smtp:
networks:
- mail
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}postfix:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
# volumes:
# - "/var/lib/mailu/overrides:/overrides"
depends_on:
- front
antispam:
hostname: antispam
networks:
- mail
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rspamd:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
volumes:
- "/var/lib/mailu/data/filter:/var/lib/rspamd"
- "/var/lib/mailu/data/dkim:/dkim"
# - "/var/lib/mailu/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
# Webmail
webmail:
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}rainloop:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
networks:
- mail
volumes:
- "/var/lib/mailu/data/webmail:/data"
depends_on:
- imap
networks:
mail:
driver: bridge
ipam:
driver: default
config:
- subnet: 192.168.203.0/24
The important things about this docker-compose.yml
file:
/var/lib/mailu
, don’t forget to adjust it if you require anything differentfront
service requires being binded to the host public IPv4 (1.2.3.4
in this post), don’t forget to replace it with your public IPv4overrides
volume mapping are commented and can be used later for any specific configuration80/tcp
and/or your 443/tcp
port(s) are not available, see the Bonus 1: Traefik enters the chat sectionmailu.env
# Mailu main configuration file
#
# Generated for compose flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
# This variable is now set directly in `docker-compose.yml by the setup utility
# ROOT=/home/stephen/data/mailu
# Mailu version to run (1.0, 1.1, etc. or master)
MAILU_VERSION=1.9
VERSION=1.9
# Set to a randomly generated 16 bytes string
SECRET_KEY=<GENERATE THIS>
# Address where listening ports should bind
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= 127.0.0.1 (default: 127.0.0.1)
# PUBLIC_IPV6= ::1 (default: ::1)
# Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)
SUBNET=192.168.203.0/24
# Main mail domain
DOMAIN=example.com
# Hostnames for this server, separated with comas
HOSTNAMES=email.example.com
# Postmaster local part (will append the main mail domain)
POSTMASTER=admin
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt)
TLS_FLAVOR=mail-letsencrypt
# Authentication rate limit (per source IP address)
AUTH_RATELIMIT=10/minute;1000/hour
# Opt-out of statistics, replace with "True" to opt out
DISABLE_STATISTICS=True
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN=true
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL=rainloop
# Dav server implementation (value: radicale, none)
WEBDAV=none
# Antivirus solution (value: clamav, none)
#ANTIVIRUS=clamav
#Antispam solution
ANTISPAM=none
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
# Max attachment size will be 33% smaller
MESSAGE_SIZE_LIMIT=50000000
# Networks granted relay permissions
# Use this with care, all hosts in this networks will be able to send mail without authentication!
RELAYNETS=
# Will relay all outgoing mails if configured
RELAYHOST=
# Fetchmail delay
FETCHMAIL_DELAY=600
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER=+
# DMARC rua and ruf email
DMARC_RUA=admin
DMARC_RUF=admin
# Welcome email, enable and set a topic and body if you wish to send welcome
# emails to all users.
WELCOME=false
WELCOME_SUBJECT=Welcome to your new email account
WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
###################################
# Web settings
###################################
# Path to redirect / to
WEBROOT_REDIRECT=/webmail
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=MyGreatMailServer
# Linked Website URL
WEBSITE=https://email.example.com
INITIAL_ADMIN_ACCOUNT=admin
INITIAL_ADMIN_DOMAIN=example.com
INITIAL_ADMIN_PW=password
INITIAL_ADMIN_MODE=ifmissing
###################################
# Advanced settings
###################################
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
# LOG_DRIVER=json-file
# Docker-compose project name, this will prepended to containers names.
COMPOSE_PROJECT_NAME=mailu
# Default password scheme used for newly created accounts and changed passwords
# (value: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=BLF-CRYPT
# Header to take the real ip from
REAL_IP_HEADER=
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
REAL_IP_FROM=
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT=
# Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
LOG_LEVEL=WARNING
###################################
# Database settings
###################################
DB_FLAVOR=sqlite
The important things about this mailu.env
file:
SECRET_KEY
must be replaced with a 16 bytes key that you need to generate (use your favourite tool for that)DOMAIN
must be adjusted to the domain you are going to use (example.com
in this post)HOSTNAMES
must be adjusted to the domain you want to use to access all of the services (email.example.com
in this post)SITENAME
must be adjusted to what you want the webmail to displayWEBSITE
must be adjusted to one for your HOSTNAMES
prefixed with the http scheme that will be used (https
+ email.example.com
= https://email.example.com
in this post)INITIAL_ADMIN_ACCOUNT
and INITIAL_ADMIN_DOMAIN
are the configuration used to create the admin account, those two value will be used to generate the admin account (admin@example.com
in this post)INITIAL_ADMIN_PW
should be changed for a more robust passwordNow that the host is configuration ready, let’s continue with the DNS configuration.
Multiple DNS are required to get everything working:
email.example.com 600 IN A 1.2.3.4
(don’t forget to adjust with your public IPv4)example.com. 600 IN MX 10 email.example.com.
, if you have another MX record with a lower priority value,this means it will be preferred and should be kept for now and adjusted/deleted once the services are upOnce those DNS created, you can bring the services up with docker-compose up -d
on your host.
After some seconds/minutes (depending on the time it takes to generate the letsencrypt certs), https://email.example.com
should be accessible.
Access https://email.example.com/admin
and connect using the admin account that was automatically created using the INITIAL_ADMIN_*
variables from the mailu.env
file. On the left, in the Administration
section, select Mail domains
(or go to https://email.example.com/admin/domain
) and select New domain
button located at the top right corner of the screen (or access https://email.example.com/admin/domain/create
).
In the form, you can:
Domain name
to create, in this post we will set example.com
but you must adjust it to your ownMaximum user count
, Maximum alias count
and Maximum user quota
to your needs for this domainEnable sign-up
and add a Comment
save
and the domain will be addedNow return to the Mail domains
page and in the Manage
columns you can create a new user. Once created you can go to the webmail page https://email.example.com/webmail
and connect using this newly created user to access your first mailbox.
Now that our mail server is up and running, we need to add more DNS configuration to protect our domain from spoofing or our mails being considered as spam by the other mail servers.
For this we will configure SPF and DKIM and use DMARC to enforce it.
Return to the Mail domains
page and, in the Actions
column, select the Details
button. You will see here some of the DNS configuration that can/needs to be set.
The DNS MX entry
line should have a green tick next to it. If not, you need to check your domain MX record (or wait a little since DNS propagation can take some time).
If you don’t have any line referring to DKIM
, it means we need to generate the keys. At the top, select Regenerate keys
to generate the keys for the DKIM configuration.
Once done, you will need to add all the mentionned DNS records:
DNS SPF entries
, a TXT record like example.com. 600 IN TXT "v=spf1 mx a:email.example.com ~all"
(if your MX record already point to email.example.com
, you can delete the a:email.example.com
configuration)DKIM public key
is not a DNS record, just the public key that we just generated and that will be used in the next entryDNS DKIM entry
, a TXT record like dkim._domainkey.example.com. 600 IN TXT "v=DKIM1; k=rsa; p=<DKIM public key>
DNS DMARC entry
, a TXT record like _dmarc.example.com. 600 IN TXT "v=DMARC1; p=reject; rua=mailto:admin@example.com; ruf=mailto:admin@example.com; adkim=s; aspf=s"
(the rua
configuration means you will get aggregate reports at admin@example.com
, ruf
will make failure reports going to admin@example.com
)This is enough for the SPF/DKIM/DMARC configuration.
In the Details
view of your example.com
mail domain, you can find a DNS TLSA entry
. This DNS record is useful only in the case your domain DNS server is using DNSSEC
to protect your domain. Most providers offer this option but it is not always enabled by default.
TLSA adds an additional layer of validation and verification for TLS connections by giving clients a way to verify the certificate received from a server by querying for its information thanks to this TLSA DNS record.
Once you made sure DNSSEC
is activated, you can create a TLSA DNS record: _25._tcp.email.example.com. 86400 IN TLSA 2 1 1 <certificate fingerprint>
(basically providing a TLSA protection for the 25/tcp
port, which is the SMTP server).
In order to check that your configuration is correct:
I am happy to see you here dear Traefik user. Looking at the configuration you might have thought “well, why do I even need this Nginx anyway?“. I know, I did too. The thing is, the Nginx server used in the front
service in the docker-compose.yml
file is required because it can be used a mail proxy. Also, it can handle an authentification delegation to a HTTP server, a thing that would require some developments since Traefik only provide a middleware for the HTTP routers (nothing - yet - for TCP routers). Finally, Traefik is not able to handle the STARTTLS
email protocol command, required by the SMTP server to perform a secure connection. Because of all of this, the only way to be able to use Mailu with Traefik is by using a TCP router.
Here are the labels that needs to be added to the front
service in the docker-compose.yml
file (don’t forget to deactivate the 80/tcp
and 443/tcp
ports declaration):
# Core services
front:
labels:
- "traefik.enable=true"
- "traefik.http.routers.mailu-http.rule=Host(`email.example.com`)"
- "traefik.http.routers.mailu-http.entryPoints=web" # adjust to the entrypoint you defined for the 80/tcp entrypoint
- "traefik.http.routers.mailu-http.priority=1001"
- "traefik.http.services.mailu-http.loadBalancer.server.port=80"
- "traefik.tcp.routers.mailu-https.rule=HostSNI(`email.example.com`)"
- "traefik.tcp.routers.mailu-https.entryPoints=websecure" # adjust to the entrypoint you defined for the 443/tcp entrypoint
- "traefik.tcp.routers.mailu-https.tls.passthrough=true"
- "traefik.tcp.services.mailu-https.loadBalancer.server.port=443"
- "traefik.docker.network=traefik" # adjust to the network you are using
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}nginx:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
networks:
- mail
logging:
driver: json-file
ports:
#- "1.2.3.4:80:80" # handled by Traefik
#- "1.2.3.4:443:443" # handle by Traefik
- "1.2.3.4:25:25"
- "1.2.3.4:465:465"
- "1.2.3.4:587:587"
- "1.2.3.4:110:110"
- "1.2.3.4:995:995"
- "1.2.3.4:143:143"
- "1.2.3.4:993:993"
volumes:
- "/var/lib/mailu/certs:/certs"
“But you said we only need a TCP router…“. Well you are right, but if, like me, you are using Traefik and its ACME configuration to take care of all your TLS certificates you are going to have an issue.
When you use the Traefik ACME configuration, a acme-http@internal
Traefik service and router are created behind the hood and will swallow any HTTP request made to any /.well-known/acme-challenge/
endpoint. This will prevent the front
service from Mailu to generate all the certs it needs. As a workaround, a HTTP router http-mailu
is added with a priority of 1001
. We also need to tell Traefik that its internal acme-http
route needs to have a priority. For this, the following labels can be added to your Traefik service declaration:
labels:
- "traefik.enable=true"
- "traefik.http.routers.acme.entrypoints=web" # adjust to the entrypoint you defined for the 80/tcp entrypoint
- "traefik.http.routers.acme.rule=PathPrefix(`/.well-known/acme-challenge/`)"
- "traefik.http.routers.acme.service=acme-http@internal"
- "traefik.http.routers.acme.priority=1000
This configuration will make sure the acme-http@internal
service is used by default and if any Traefik HTTP router is defined with a higher priority, it will take care of the ACME challenge itself.
Restart Traefik and then your Mailu services, and you will be good to go.
MTA-STS
is another security protocol used to prevent some holes (man in the middle mostly) from the STARTTLS
use.
To use MTA-STS
, you will need to:
mta-sts.example.com. 600 IN CNAME email.example.com.
_mta-sts.example.com. 600 IN TXT "v=STSv1; id=1"
(for any change in the MTA-STS configuration, you will need to increment the id
to cancel the cache of the other clients)mta-sts.txt
file from your mta-sts.example.com
domainThe 3. requirement can be done using Mailu and its overrides
capability. Next to your docker-compose.yml
and mailu.env
files, create a overrides/nginx
folder and, in that folder, create a mta-sts.conf
file with the following content:
location ^~ /.well-known/mta-sts.txt {
return 200 "version: STSv1
mode: enforce
max_age: 604800
mx: email.example.com\r\n";
}
Once the mta-sts.conf
(lets say its full path is /path/to/my/mailu/overrides/nginx/mta-sts.conf
) file is created, adjust your docker-compose.yml
file to consider this new Nginx configuration file:
# Core services
front:
image: ghcr.io/${DOCKER_ORG:-mailu}/${DOCKER_PREFIX:-}nginx:${MAILU_VERSION:-1.9}
restart: always
env_file: mailu.env
networks:
- mail
logging:
driver: json-file
ports:
- "1.2.3.4:80:80"
- "1.2.3.4:443:443"
- "1.2.3.4:25:25"
- "1.2.3.4:465:465"
- "1.2.3.4:587:587"
- "1.2.3.4:110:110"
- "1.2.3.4:995:995"
- "1.2.3.4:143:143"
- "1.2.3.4:993:993"
volumes:
- "/var/lib/mailu/certs:/certs"
- "/path/to/my/mailu/overrides/nginx:/overrides" # don't forget to adjust /path/to/my/mailu
After that, and because Mailu is taking care of all the certs, we need to add the mta-sts.example.com
domain in the HOSTNAMES
variable from the mailu.env
file:
...
# Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)
SUBNET=192.168.203.0/24
# Main mail domain
DOMAIN=example.com
# Hostnames for this server, separated with comas
HOSTNAMES=email.example.com,mta-sts.example.com
...
In case you are using Traefik, you will need to adjust the labels
to consider this mta-sts.example.com
domain.
Then you would need to docker-compose stop front && docker-compose rm front && docker-compose up -d
to make sure the new volume
is considered.
Once all Mailu services are deployed and healthy, you can use curl
to check that https://mta-sts.example.com/.well-known/mta-sts.txt
is returning the MTA-STS configuration:
curl 'https://mta-sts.example.com/.well-known/mta-sts.txt'
version: STSv1
mode: enforce
max_age: 604800
mx: email.example.com
Finally, this free tool can be used to test your MTA-STS configuration is correct.
imapsync
In case you want to migrate some previous mailboxes to your brand new mailboxes, you can use the imapsync project.
I used the Docker version of the tool to perform multiple migrations that can be easily done using the following commande line:
docker run --rm gilleslamiral/imapsync imapsync --host1 old.example.com --user1 test1 --password1 'secret1' --host2 email.example.com --user2 test1@example.com --password2 'secret2'
You would need to 1st create the new mailboxes in your email.example.com
admin panel before being able to use it as a destination mailbox (host2
, user2
, password2
parameters).