Running Docker Mailserver yourself | a field report


With the help of a suitable Docker image, it is relatively easy to run a mail server yourself. I originally used the integrated mail server of the Host Europe vServer (Plesk) and came across a very simple Docker container while looking for a replacement. The lightweight container provides a mail server without a graphical management interface, but can be managed with a few simple commands. Any email client can be used to send and receive the mails, for this POP3 or IMAP is offered for receiving and SMTP for sending. Whether the mails of the own server can be received and delivered, depends on the setup, on the correct DNS settings. 

current version 14.0.0

Docker Basics

Docker allows applications to be launched by command in a so-called container.
A container is an isolated environment independent of the operating system (OS):
When a container is first launched, Docker independently loads all the necessary sources
from the internet.
Docker can be installed on Windows, macOS or an Linux Distribution


In advance, I installed Docker, see: topic/docker and for the SSL certificates I have a reverse proxy in use: traefik-reverse-proxy.

Mail server commissioning

I used the following Docker image as my mail server: "".

There are 3 files required to get started:

  • docker-compose.yml
  • .env (The variables of the .env file could also be replaced directly in the docker-compose.yml file)
  • .mailserver.env (variables for the mail server settings).

I created the docker-compose file as follows:

    hostname: ${HOSTNAME}
    domainname: ${DOMAINNAME}
    container_name: ${CONTAINER_NAME}
    env_file: mailserver.env
      - "25:25"
      - "143:143"
      - "587:587"
      - "993:993"
      - "4190:4190"
      - ./docker-data/dms/maildata:/var/mail
      - ./docker-data/dms/mailstate:/var/mail-state
      - ./docker-data/dms/maillogs:/var/log/mail
      - ./docker-data/dms/config/:/tmp/docker-mailserver/
      - ./docker-data/dms/cron/sa-learn:/etc/cron.d/sa-learn
      - /etc/localtime:/etc/localtime:ro
    restart: always
    cap_add: [ "NET_ADMIN", "SYS_PTRACE" ]

In addition, I use a separate file for the variables in the docker-compose.yml file: .env, this contains the values for the host name, domain name and container name:


And lastly, the mailserver.env file is needed for the setup, this can be downloaded from the git repository:

wget -O mailserver2.env

With this, the server would basically be ready for use and could already be started. Could be, because for additional settings the file mailserver.env should also be briefly reviewed and features required therein should be activated or deactivated. The file is relatively well commented and thus largely self-explanatory. In order for the mails to be delivered and received, the corresponding DNS records must also be created.

The container is then started with the command "docker-compose up".

root@l2:~/mailserver# docker compose up -d
Status: Downloaded newer image for mailserver/docker-mailserver:latest
Creating mailserver ... done
Attaching to mailserver


I have adjusted the following settings in the mailserver.env file:


For SSL_TYPE=letsencrypt, see: ssl



By default the size of the mails is limited to 10MB, by setting the variable “POSTFIX_MESSAGE_SIZE_LIMIT” the limit can be increased.


New email addresses or forwardings can be created using the setup script. Since version 10.2 the script is located in the Docker container, accordingly the call is done via “docker exec”. The help can be displayed as follows:

docker exec mailserver setup help

“mailserver” in the command stands for the name of the Docker container, here is the displayed help for version 11.2

root@ubuntu:/var/mailserver# docker exec mailserver setup help

    setup - 'docker-mailserver' Administration & Configuration script

    ./setup [ OPTIONS... ] COMMAND [ help | ARGUMENTS... ]

    COMMAND := { email | alias | quota | dovecot-master | config | relay | debug } SUBCOMMAND

    This is the main administration script that you use for all your interactions with
    'docker-mailserver'. Setup, configuration and much more is done with this script.

    Please note that this script executes most of its commands inside the container itself.
    If it cannot find a running 'docker-mailserver' container, it will attempt to run one using
    any available tags which include 'label=org.opencontainers.image.title="docker-mailserver"'
    and then run the necessary commands. If the tag for the container is not found, this script
    will pull the ':latest' tag of ''.
    This tag refers to the latest release, see the tagging convention in the README under:

    You will be able to see detailed information about the script you're invoking and their
    arguments by appending 'help' after your command. Currently, this does not work with all scripts.

    COMMAND email :=
        /usr/local/bin/setup email add <EMAIL ADDRESS> [<PASSWORD>]
        /usr/local/bin/setup email update <EMAIL ADDRESS> [<PASSWORD>]
        /usr/local/bin/setup email del [ OPTIONS... ] <EMAIL ADDRESS> [ <EMAIL ADDRESS>... ]
        /usr/local/bin/setup email restrict <add|del|list> <send|receive> [<EMAIL ADDRESS>]
        /usr/local/bin/setup email list

    COMMAND alias :=
        /usr/local/bin/setup alias add <EMAIL ADDRESS> <RECIPIENT>
        /usr/local/bin/setup alias del <EMAIL ADDRESS> <RECIPIENT>
        /usr/local/bin/setup alias list

    COMMAND quota :=
        /usr/local/bin/setup quota set <EMAIL ADDRESS> [<QUOTA>]
        /usr/local/bin/setup quota del <EMAIL ADDRESS>

    COMMAND dovecot-master :=
        /usr/local/bin/setup dovecot-master add <USERNAME> [<PASSWORD>]
        /usr/local/bin/setup dovecot-master update <USERNAME> [<PASSWORD>]
        /usr/local/bin/setup dovecot-master del [ OPTIONS... ] <USERNAME> [ <USERNAME>... ]
        /usr/local/bin/setup dovecot-master list

    COMMAND config :=
        /usr/local/bin/setup config dkim [ ARGUMENTS... ]

    COMMAND relay :=
        /usr/local/bin/setup relay add-auth <DOMAIN> <USERNAME> [<PASSWORD>]
        /usr/local/bin/setup relay add-domain <DOMAIN> <HOST> [<PORT>]
        /usr/local/bin/setup relay exclude-domain <DOMAIN>

    COMMAND fail2ban :=
        /usr/local/bin/setup fail2ban
        /usr/local/bin/setup fail2ban ban <IP>
        /usr/local/bin/setup fail2ban unban <IP>

    COMMAND debug :=
        /usr/local/bin/setup debug fetchmail
        /usr/local/bin/setup debug login <COMMANDS>
        /usr/local/bin/setup debug show-mail-logs

    ./ email add
        Add the email account You will be prompted
        to input a password afterwards since no password was supplied.

    ./ config dkim keysize 2048 domain ','
        Creates keys of length 2048 but in an LDAP setup where domains are not known to
        Postfix by default, so you need to provide them yourself in a comma-separated list.

    ./ config dkim help
        This will provide you with a detailed explanation on how to use the
        config dkim command, showing what arguments can be passed and what they do.

Create mail addresses

docker exec mailserver setup email add email@domain password

After the first creation of an email address for a new domain, the DKIM key for the DNS record can be created, see DKIM.

Create alias (forwarding)

docker exec mailserver setup alias add email@domain.tld weiterleitung@mail-adresse.ok


After the first mail addresses or forwardings have been created on the system, DKIM Keys can be generated with the following command:

docker exec mailserver setup config dkim

The keys are stored for all domains for which an email address or forwarding was created in the following folder: config/opendkim/keys/domain.tld/mail.txt. The keys should then be used for the DNS entry:

DNS entries

For the operation of a mail server, the DKIM key, an SPF record and a DMARC policy should be created in the DNS in addition to the A and MX record. The domain name: domain.tld is representative for the own domain in the following examples. The following entries must be added to the DNS zone for domain.tld:


Type Name Value
A mail.domain.tld IP address of the web server

In addition, mail.domain.tld should be stored as the server name in the reverse lookup zone so that the IP address of the web server also resolves to mail.domain.tld. For a vServer or cloud server of a provider, the server name can be set in the admin interface of the provider.


Type Name Value Priorität
MX mail.domain.tld IP address of the web server  

The MX record is responsible for delivering the mails. All mails that have @domain.tld as domain will be delivered to this server. If there are several mail servers, the server with the lowest priority is used.

The previously created DKIM Key:

Type Name Value
TXT mail._domainkey.domain.tld v=DKIM1; h=sha256; k=rsa; p=Content-From-config/opendkim/keys/domain.tld/mail.txt without "

DKIM (DomainKeys Identified Mail) is a signature that is added to mails to make it more difficult to forge the sender. DKIM is used for sending mails. If the provider only supports 512 characters for its DNS entries, a shorter key can be generated with the following call: "docker exec mailserver setup config dkim keysize 2048". For the keys to be reissued, the old keys in the folder ..config/opendkim/keys/domain.tld must be deleted beforehand and the mail server restarted.


Type Name Value
TXT @ “v=spf1 ip4:IP-Adresse-des-Webservers -all"

To use the ip4 setting of another domain, the SPF entry can be redirected as follows. It is important that the -all parameter is not used: 

Type Name Value
TXT @ “v=spf1 redirect=server.domain.tld"

The SPF entry (Sender Policy Framework) is also a measure to make it more difficult to forge email addresses. The SPF entry specifies which servers are authorized to send the emails, in the example the IP address of our mail server.


Type Name Value
TXT _dmarc.domain.tld “v=DMARC1; p=reject; rua=mailto:report@domain.tld"

DMARC builds on SPF and DKIM and specifies how the server to which mail is sent should authenticate the email.

Check DNS

The easiest way to test the DNS records is on the site:

As an example for the domain: domain.tld

MX Lookup

The MX lookup on the mxtoolbox page should look something like this:

Pref Hostname IP Address TTL  
10 mail.domain.tld ???.???.???.??? 24 hrs Blacklist Check      SMTP Test
  Test Result
DMARC Record Published DMARC Record found
DMARC Policy Not Enabled DMARC Quarantine/Reject policy enabled
DNS Record Published DNS Record found


Here is an SMTP test on mxtoolbox:

smtp:IP addressMailserver

220 mail.domain.tld ESMTP

  Test Result
SMTP Reverse DNS Mismatch OK - ???.???.???.??? resolves to domain.tld
SMTP Valid Hostname OK - Reverse DNS is a valid Hostname
SMTP Banner Check OK - Reverse DNS matches SMTP Banner
SMTP TLS OK - Supports TLS.
SMTP Connection Time 0.303 seconds - Good on Connection time
SMTP Open Relay OK - Not an open relay.
SMTP Transaction Time 1.006 seconds - Good on Transaction Time

Email to a Google address

As an example, the email from your own web server can also be sent to a Google account:

Here, too, a status for SPF, DKIM and DMARC then appears under “Show original”:

Original Message

Message ID <??????.??????.???x.???>
Created at: Thu, Apr 8, 2021 at  06:55 AM (Delivered after 2 seconds)
From: First name Last name <user@domain.tld>
To: “Firstname Lastname (” <>
Subject: test - dns
SPF: PASS with IP ???.???.???.??? 
DKIM: 'PASS' with domain domain.tld

SSL Certificates

As setup for SSL certificates I use Let's Encrypt and a reverse proxy, see: Traefik-Reverse-Proxy. To use the certificate management from the reverse proxy, I modified the docker-compose.yml file as follows:

    hostname: ${HOSTNAME}
    domainname: ${DOMAINNAME}
    container_name: ${CONTAINER_NAME}
    env_file: mailserver.env
      - "25:25"
      - "143:143"
      - "587:587"
      - "993:993"  
      - "4190:4190"
      - ./docker-data/dms/maildata:/var/mail
      - ./docker-data/dms/mailstate:/var/mail-state
      - ./docker-data/dms/maillogs:/var/log/mail
      - ./docker-data/dms/config/:/tmp/docker-mailserver/
      - ./docker-data/dms/cron/sa-learn:/etc/cron.d/sa-learn
      - ../traefik/letsencrypt/acme.json:/etc/letsencrypt/acme.json:ro      
      - /etc/localtime:/etc/localtime:ro
    restart: always
    cap_add: [ "NET_ADMIN", "SYS_PTRACE" ]
    image: nginx
      - "traefik.enable=true"
      - "traefik.http.routers.mailserver.rule=Host(`${HOSTNAME}`)"   
      - "traefik.http.routers.mailserver.entrypoints=web"
      - "traefik.http.routers.mailserver.entrypoints=websecure"
      - "traefik.http.routers.mailserver.tls.certresolver=myresolver"  
      - proxy-tier
    restart: always

           external: true
           name: webproxy

The path to the nginx-data folder of the web proxy under “Volumes” must of course be adjusted accordingly. In addition, unlike the original docker-compose file, I have redirected the other volumes to the file system.  See: Docker data storage: Docker Volumes vs. Bind Mounts

SpamAssassin Spamfilter learn

To cope with the flood of spam mails, I trained SpamAssassin with the sa-learn command: SpamAssassin can learn from existing mails from the inbox and the junk folder besides blacklists and other spam checks. The sa-learn command examines both folders for this purpose and remembers certain patterns of spam and non-spam mails (ham). So if a mail is detected wrong and then moved to the spam folder by the mail client, SpamAssassin can learn from it and put a similar mail in the spam folder next time.

The sa-learn command can be launched in the Docker container as follows:

root@server:~# docker exec mailserver sa-learn --ham /var/mail/*/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin
Learned tokens from 0 message(s) (1375 message(s) examined)
root@server:~# docker exec mailserver sa-learn --spam /var/mail/*/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin
Learned tokens from 22 message(s) (705 message(s) examined)

/var/*/*/cur* searches all inboxes and their subfolders, /var/mail/*/*/.Junk all spam folders of all mailboxes.

In order to start the learning command regularly, the cron service integrated in the container can be used and the following file can be created for this purpose:


0  2 * * * root  sa-learn --ham /var/mail/*/*/cur* --dbpath /var/mail-state/lib-amavis/.spamassassin
0  3 * * * root  sa-learn --spam /var/mail/*/*/.Junk --dbpath /var/mail-state/lib-amavis/.spamassassin

SIEVE - Filter rules

With the help of Sieve filters it is possible to apply certain rules to the inbox, e.g. move certain senders to a folder. Here is an example of my filter rules used in Nextcloud:

require ["fileinto", "mailbox","envelope","reject"];
if anyof (address :contains "From" [
    header :contains "Subject" [
          "other SPAM-Keyword",
     fileinto :create "Junk";
elsif anyof (address :matches "From" [
     fileinto :create "Newsletter";

In order for Sieve to be changed via the filter rules using the mail client, port 4190 must be stored in docker-compose.

Fetch Mail - Get mails from other accounts

To regularly fetch emails from another account, “ENABLE_FETCHMAIL” can be enabled in the mailserver.env file:

# –––––––––––––––––––––––––––––––––––––––––––––––
# ––– Fetchmail Section –––––––––––––––––––––––––
# –––––––––––––––––––––––––––––––––––––––––––––––


# The interval to fetch mail in seconds

The default value for fetching is every 5 minutes.

The account data for fetching can then be stored in the following config file: docker-data/dms/config/

## Example configuration: IMAP
poll imap.domain.tld with proto IMAP
	user '' there with
	password 'pwd'
	is 'mailboxOnDockerMailserver@domain.tld'
	here ssl

Debug - Fail2ban

If the Fail2ban service is enabled, failed login attempts will be penalized by blocking the sender for some time. The blocked IP addresses can be displayed and unblocked as follows:

docker exec mailserver setup fail2ban
Banned in dovecot: ???.???.???.???
Banned in postfix-sasl:,,,,,
root@ubuntu-4gb-nbg1-2:/var/web/mailserver# ./ debug fail2ban unban ???.???.???.???
Unbanned IP from dovecot: ???.???.???.???

Log output

As for other Docker containers, docker logs can be used to display what the container is currently doing:

docker logs -f mailserver

Troubleshooting: DNS records

Policy Reasons

Mail Delivery Subsystem 

Returned mail: see transcript for details

This message was created automatically by mail delivery software.

Deny to deliver the message you sent to one or more recipients.

Reasons for deny are as follows:

REASONS: Policy Reasons


Possible cause: missing SPF record

Domain unknown

An email to an unknown domain is presented approximately like this by the mail delivery subsystem:

Mail Delivery Subsystem 

Returned mail: see transcript for details

Undelivered Mail Returned to Sender

This is the mail system at host mail.domain.tld.

I'm sorry to have to inform you that your message could not
be delivered to one or more recipients. It's attached below.

For further assistance, please send mail to postmaster.

If you do so, please include this problem report. You can
delete your own text from the attached returned message.

                   The mail system

Quoted text
>: Host or domain name not found. Name service error
    for name=invalid.domain type=AAAA: Host not found

Delivery report


Delivery report

Hello, this is the mail server on ???.???.???

I am sending you this message to inform you on the delivery status of a

message you previously sent.  Immediately below you will find a list of

the affected recipients;  also attached is a Delivery Status Notification

(DSN) report in standard format, as well as the headers of the original


Quoted text

>  delivery failed; will not continue trying


As usual for Docker, new versions can be loaded with a pull:

docker-compose pull
docker-compose up -d


Admittedly, administration via commands in the terminal is certainly not everyone's cup of tea. However, the advantage of this setup is that the configuration and the data of the complete mail server are located in one folder thanks to Docker. And the server can thus be copied to another server very easily and started there, see also: topic/docker

positive Bewertung({{pro_count}})
Rate Post:
{{percentage}} % positive
negative Bewertung({{con_count}})

THANK YOU for your review!

Questions / Comments

By continuing to browse the site, you agree to our use of cookies. More Details