How To Set Up A Mailserver Within A Docker Swarm
Tags: docker, devops, opensource, webdev
Published: 2024-01-19 07:48:26

**Ever wanted to have your own mail server? Learn how to set up your own personal mail server with this step-by-step guide**

I run my own mail server to have generalized email addresses for different services.

While I searched for a nice solution to put my mail server into a dockerized mail server I found the famous [docker-mailserver](https://github.com/docker-mailserver?ref=paulsblog.dev). Unfortunately, I could not use this mail server because I had many errors while configuring it.  
Because of this, I searched for an alternative and found [hardware-mailserver](https://github.com/hardware/mailserver?ref=paulsblog.dev) which is something like an optimized usage of a docker-mailserver with multiple predefined functionalities.

If I had a normal docker environment without any other services I could use [this docker-compose.yml](https://github.com/hardware/mailserver/blob/master/docker-compose.sample.yml?ref=paulsblog.dev) and start it by executing:
```
docker-compose up -d
```

Because [I run a Docker Swarm](https://www.paulsblog.dev/docker-swarm-in-a-nutshell/) environment with [a Traefik load balancer](https://www.paulsblog.dev/services-you-want-to-have-in-a-swarm-environment/#traefik) that creates SSL certificates for my domains I have to make some adjustments within the Compose file to set up and configure the mail server. Every adjustment will be explained later on service by service.

Mailserver
----------
```yaml
mailserver:
    image: hardware/mailserver:1.1-stable
    restart: ${MAILSERVER_RESTART_MODE}
    domainname: ${MAILSERVER_DOMAIN}
    hostname: ${MAILSERVER_HOSTNAME} 
    deploy:
        placement:
          constraints:
            - node.role == manager
        labels:
          - traefik.enable=true
          - traefik.docker.network=traefik-public
          - traefik.constraint-label=traefik-public
          - traefik.frontend.rule=Host:mail.${MAILSERVER_DOMAIN}
          - traefik.http.routers.mailserver-spam-http.rule=Host(`${MAILSERVER_FQDN?Variable not set}`) || Host(`pop3.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`smtp.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`imap.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`${MAILSERVER_DOMAIN_RSPAMD?Variable not set}`)
          - traefik.http.routers.mailserver-spam-http.entrypoints=http
          - traefik.http.routers.mailserver-spam-http.middlewares=https-redirect
          - traefik.http.routers.mailserver-spam-https.rule=Host(`${MAILSERVER_FQDN?Variable not set}`) ||  Host(`pop3.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`smtp.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`imap.${MAILSERVER_DOMAIN?Variable not set}`) || Host(`${MAILSERVER_DOMAIN_RSPAMD?Variable not set}`)
          - traefik.http.routers.mailserver-spam-https.entrypoints=https
          - traefik.http.routers.mailserver-spam-https.tls=true
          - traefik.http.routers.mailserver-spam-https.tls.certresolver=le
          - traefik.http.services.mailserver-spam.loadbalancer.server.port=11334
    ports:
      - "25:25"         # SMTP                - Required
      - "110:110"     # POP3           STARTTLS - Optional - For webmails/desktop clients
      - "143:143"     # IMAP           STARTTLS - Optional - For webmails/desktop clients
      - "465:465"     # SMTPS         SSL/TLS  - Optional - Enabled for compatibility reason, otherwise disabled
      - "587:587"     # Submission  STARTTLS - Optional - For webmails/desktop clients
      - "993:993"     # IMAPS          SSL/TLS  - Optional - For webmails/desktop clients
      - "995:995"     # POP3S          SSL/TLS  - Optional - For webmails/desktop clients
      - "4190:4190" # SIEVE            STARTTLS - Optional - Recommended for mail filtering
    environment:
      - FQDN=${MAILSERVER_FQDN}
      - DOMAIN=${MAILSERVER_DOMAIN}
      - DBPASS=${MAILSERVER_DATABASE_USER_PASSWORD}       # MariaDB database password (required)
      - RSPAMD_PASSWORD=${MAILSERVER_RSPAMD_PASSWORD}     # Rspamd WebUI password (required)
    #- ADD_DOMAINS=aa.tld, www.bb.tld...      # Add additional domains separated by commas (needed for dkim keys etc.)
      - ENABLE_POP3=true                       # Enable POP3 protocol
    #
    # Full list : https://github.com/hardware/mailserver#environment-variables
    #
    volumes:
      - mail:/var/mail
      #- ./cert:/etc/letsencrypt/live/${MAILSERVER_FQDN}
    depends_on:
      - mariadb
      - redis
    networks:
      - traefik-public
      - default
```
This is the main service used by the mail server suite. The most important thing I had to add was the environment variables. Because I run a Docker Swarm setup hostname does not work correctly and I have to develop something else. Within a pull request on the GitHub page, I found a possible solution.

I have to add `FQDN` and `DOMAIN` as environment variables. Another section I have to change is the labels section. I added the `deploy` section and created two important properties:

*   `placement-constraint` and
*   `labels`.

`placement-constraints` are used to always deploy the mail server on my manager node and `labels` is filled with all information that is needed for my Traefik instance.

Finally, I have to change the volume entries. I want to use docker volumes instead of a local shared folder (which is achieved by adding a `./` in front of the volume name)

**Also, there is one very important entry within the volumes section:**
```yaml
      #- ./cert:/etc/letsencrypt/live/${MAILSERVER_FQDN}
```

I will explain what it does and why its comment out later in detail

Postfix-admin
-------------

```yaml
  postfixadmin:
    image: hardware/postfixadmin
    restart: ${MAILSERVER_RESTART_MODE}
    domainname: ${MAILSERVER_DOMAIN}
    hostname: ${MAILSERVER_HOSTNAME}
    deploy:
        placement:
          constraints:
            - node.role == manager
        labels:
          - traefik.enable=true
          - traefik.docker.network=traefik-public
          - traefik.constraint-label=traefik-public
          - traefik.http.routers.mailserver-postfixadmin-http.rule=Host(`${MAILSERVER_DOMAIN_POSTFIXADMIN?Variable not set}`)
          - traefik.http.routers.mailserver-postfixadmin-http.entrypoints=http
          - traefik.http.routers.mailserver-postfixadmin-http.middlewares=https-redirect
          - traefik.http.routers.mailserver-postfixadmin-https.rule=Host(`${MAILSERVER_DOMAIN_POSTFIXADMIN?Variable not set}`)
          - traefik.http.routers.mailserver-postfixadmin-https.entrypoints=https
          - traefik.http.routers.mailserver-postfixadmin-https.tls=true
          - traefik.http.routers.mailserver-postfixadmin-https.tls.certresolver=le
          - traefik.http.services.mailserver-postfixadmin.loadbalancer.server.port=8888
    environment:
      - DBPASS=${MAILSERVER_DATABASE_USER_PASSWORD}
      - FQDN=${MAILSERVER_FQDN}
      - DOMAIN=${MAILSERVER_DOMAIN}
    depends_on:
      - mailserver
      - mariadb
    networks:
      - traefik-public
      - default
```

Like I did before I have to add environment variables `FQDN` and `DOMAIN`, adjust the `placement-constraint`, `labels` and updated the volumes.

Rainloop
--------
```yaml
  rainloop:
    image: hardware/rainloop
    restart: ${MAILSERVER_RESTART_MODE}
    deploy:
        placement:
          constraints:
            - node.role == manager
        labels:
          - traefik.enable=true
          - traefik.docker.network=traefik-public
          - traefik.constraint-label=traefik-public
          - traefik.http.routers.mailserver-rainloop-http.rule=Host(`${MAILSERVER_DOMAIN_RAINLOOP?Variable not set}`)
          - traefik.http.routers.mailserver-rainloop-http.entrypoints=http
          - traefik.http.routers.mailserver-rainloop-http.middlewares=https-redirect
          - traefik.http.routers.mailserver-rainloop-https.rule=Host(`${MAILSERVER_DOMAIN_RAINLOOP?Variable not set}`)
          - traefik.http.routers.mailserver-rainloop-https.entrypoints=https
          - traefik.http.routers.mailserver-rainloop-https.tls=true
          - traefik.http.routers.mailserver-rainloop-https.tls.certresolver=le
          - traefik.http.services.mailserver-rainloop.loadbalancer.server.port=8888
    volumes:
      - rainloop:/rainloop/data
    depends_on:
      - mailserver
      - mariadb
    networks:
      - traefik-public
      - default
```

I only add the `deploy` section and change volumes.

MariaDB
-------
```yaml
  mariadb:
    image: mariadb:10.2
    hostname: mariadb.db
    restart: ${MAILSERVER_RESTART_MODE}
    # Info : These variables are ignored when the volume already exists (if databases was created before).
    environment:
      - MYSQL_RANDOM_ROOT_PASSWORD=yes
      - MYSQL_DATABASE=postfix
      - MYSQL_USER=postfix
      - MYSQL_PASSWORD=${MAILSERVER_DATABASE_USER_PASSWORD}
    deploy:
      placement:
        constraints:
          - node.role == manager
    volumes:
      - sql:/var/lib/mysql
    networks:
      - default
```

The MariaDB container gets a `placement-constraint` so I don't lose data and docker volumes that are used. Additionally, it is **very important** that the `MYSQL_PASSWORD` is the same as defined within the mailserver-service.

Redis
-----
```yaml
  redis:
    image: redis:4.0-alpine
    hostname: redis.db
    restart: ${MAILSERVER_RESTART_MODE}
    command: redis-server --appendonly yes
    deploy:
        placement:
            constraints:
              - node.role == manager
    volumes:
      - redis:/data
    networks:
      - default
```

The last service is the Redis container which gets a `placement-constraint` and adjusted volumes.

Going Further
-------------

After the services were adjusted within one file (`docker-compose.mailserver.yml`) the next step was to deploy the Docker Swarm stack. Because I declared several environment variables I had to export them first (and I have to export them every time I recreate the service):

```bash
export MAILSERVER_DOMAIN=$PRIMARY_DOMAIN
export MAILSERVER_HOSTNAME=mail
export MAILSERVER_DOMAIN_POSTFIXADMIN=postfixadmin.$MAILSERVER_DOMAIN
export MAILSERVER_DOMAIN_RSPAMD=mail-spam.$MAILSERVER_DOMAIN
export MAILSERVER_DOMAIN_RAINLOOP=emails.$MAILSERVER_DOMAIN
export MAILSERVER_FQDN=$MAILSERVER_HOSTNAME.$MAILSERVER_DOMAIN
export MAILSERVER_DATABASE_USER_PASSWORD=lkhsaf98oihn53oysf9usaih5nb3rysa98fh3lnoiushdfisdugfbn32thsd9gpiesgs
export MAILSERVER_RSPAMD_PASSWORD=wsdofytesdf#*TYFkjhasd
export MAILSERVER_DOCKER_TAG=1.1-stable
export MAILSERVER_RESTART_MODE=unless-stopped
```

After exporting these variables I could deploy the mail server to my Docker Swarm by executing:
```bash
docker stack deploy -c docker-compose.mailserver.yml mailserver
```

Now the mail server was deployed BUT it does not use SSL. This is because the certificates are generated by traefik the first time **AFTER** the website was visited for the first time. Because I declared within the mailserver-service that there is a domain called `mail.$PRIMARY_DOMAIN`. I opened this website and it showed rspam BUT it also creates the SSL certificate which is needed for the mail server.

![Mailserver is deployed with Docker within Docker Swarm but need SSL](https://www.paulsblog.dev/content/images/2022/09/image--24-.webp)

How To Use SSL?
---------------

For the prior created SSL certificates I need a function to transfer them to the mail server. Unfortunately, the automatic transfer process from the mail server could not be used because I installed the second version of Traefik.

I had to create a small script that uses dumpcerts (from the mailserver GitHub) for extracting certs from traefik-acme.json and storing them into a file that the mail server can use to have an SSL certificate:
```bash
#!/bin/sh

mkdir -p /tmp/dump

./dumpcerts.traefik.v2.sh ./acme.json /tmp/dump
cp /tmp/dump/certs/$MAILSERVER_FQDN.crt cert/fullchain.pem
cp /tmp/dump/private/$MAILSERVER_FQDN.key cert/privkey.pem
```

This script assumes that the `acme.json` file that traefik uses to store certificates is located in the same folder as the mail server. The dumpcert script can be found [within the GitHub repository](https://github.com/hardware/mailserver/blob/master/rootfs/usr/local/bin/dumpcerts.traefik.v2.sh?ref=paulsblog.dev).

If the scripts exit without error the mail server can be turned down and redeployed (because SSL data is only checked at the start of the mail server stack ).

Additionally, I created an update script that uses all commands together:
```bash
./update-cert
docker stack rm mailserver
docker stack deploy -c docker-compose.mailserver.yml mailserver
```

Unfortunately, you have to **do this every three months** because traefik generated certs only have a life span of three months.

After install stuff
-------------------

If everything is started the next task is configuring `postfix-admin` and `rainloop`. There are two very easy info pages for this where you can find all information about configuring these services: [rainloop-initial](https://github.com/hardware/mailserver/wiki/Rainloop-initial-configuration?ref=paulsblog.dev), [postfix-initial](https://github.com/hardware/mailserver/wiki/Postfixadmin-initial-configuration?ref=paulsblog.dev)

Furthermore, the DNS setup is required so that the mail server works! **This step is very important.**

![Set important DNS records to have a good score for your mailserver](https://www.paulsblog.dev/content/images/2022/09/image--25-.webp)

Notes:

*   Make sure that the **PTR record** of your IP matches the FQDN (default: mail.domain.tld) of your mail server host. This record is usually set in your web hosting interface.
*   DKIM, SPF, and DMARC records are recommended to build a good reputation score.
*   DMARC record can be created [on Dmarcian.com](https://dmarcian.com/dmarc-record-wizard/?ref=paulsblog.dev)
*   SPF needs the public IP of the mail server!: `v=spf1 a mx ipYOURMAINHERE ~all`
*   The DKIM public (`mail._domainkey`) key will be available on the host after the container startup here:

```
/var/lib/docker/volumes/mailserver_mail/_data/dkim/domain.tld/public.key
```

![Docker Swarm mailserver is installed and set up with SSL, PTR record, DKIM, SPF, DMARC](https://www.paulsblog.dev/content/images/2022/09/image--26-.webp)

Closing Notes
-------------

Now the mail server is running and you can use postfix admin (postfixadmin.yourdomain.de) to create accounts that can be accessed with Rainloop (webmail.yourdomain.de) or another email client (Thunderbird/Betterbird).

I hope you find this tutorial helpful and are now able to add a mail server to your Docker Swarm environment.

Also, if you have any questions, ideas, recommendations, or want to share cool Docker commands or tools, please contact me. I try to answer your question if possible and will test your recommendations.

This article was originally published on my blog at [https://www.paulsblog.dev/how-to-set-up-a-mailserver-within-a-docker-swarm/](https://www.paulsblog.dev/how-to-set-up-a-mailserver-within-a-docker-swarm/)

Feel free to connect with me on [my blog](https://www.paulsblog.dev), [Medium](https://medium.knulst.de/), [LinkedIn](https://www.linkedin.com/in/paulknulst/), [Twitter](https://twitter.com/paulknulst), and [GitHub](https://github.com/paulknulst).

* * *

**🙌 Support this content**

If you like this content, please consider [supporting me](https://www.paulsblog.dev/support/). You can share it on social media or [buy me a coffee](https://buymeacoffee.com/paulknulst)! Any support helps!

Furthermore, you can [sign up for my newsletter](https://www.paulsblog.dev/#/portal/signup/) to show your contribution to my content. See the [contribute page](https://www.paulsblog.dev/contribute/) for all (free or paid) ways to say thank you!

Thanks! 🥰

* * *

Photo by [Wayhomestudio](https://www.freepik.com/photos/watching-movie) / [Freepik](https://www.freepik.com)