Hosting your own email with docker

2nd April, 2026 - Posted by david

Introduction

Ever since I learned how to properly host a website myself (i.e. rent my own VM, setup a web server and firewall, point DNS to server, serve web pages), I wanted to also host my own email. When I looked at this 10-15 years ago for menucosm, all the advice I found online could be summed up as: don’t bother, it’s too much hassle and not worth the €60/year it cost at the time to pay Google to host it, so that’s what I did.

Last year, I started working on a side-hustle, selling custom Lego sets I designed myself. Since this was a side-hustle and I was already paying for my own web server, I decided to try and keep things as lean as possible, which meant hosting the e-commerce site myself. No issues here: I found a PHP e-commerce package, installed it and wrote my own custom theme. Obviousy I would need email for this, for payments, orders, contacts etc.

I’d dockerized my setup on my web server over the years, so I now have a number of sites, all dockerized, sitting behind a reverse proxy webserver, powered by the excellent nginx-proxy. One of the great things about nginx-proxy is that it can handle SSL certificates for you automatically via Let’s Encrypt, for all your sites behind the proxy server, very convenient.

Being behind nginx-proxy is the only slightly unusual thing about my setup that might not apply to everybody trying to follow this guide. I use it for the SSL certs, but Docker Mailserver (see next paragraph) also can handle this for you, but I’ll let you figure out how yourself.

The great thing about docker and virtualization is that plenty of people have built complicated but reusable images. Since it was 2025, I knew there must be dockerized email setups and sure enough there are. The one I went for in the end was Docker Mailserver. I also needed a UI for webmail and configuration, so after I read about Rainloop, I found a docker image for that also. Now I just needed to set it all up.

For the sake of this blog post, let’s say my website is example.com and my mail server will be mail.example.com.

I’ve been using this setup for about 9 months so far, and as far as I can see, all has been running smoothly. From the very beginning, Google was accepting my test emails (i.e. from my new webmail to my existing gmail account), not marking anything as spam. I receive emails directly and through the website and these seem to be delivered successfully also. All good as far as I know.

One important thing before starting

Many hosting providers, including my own, have port 25 blocked for all outgoing connections. This is to prevent a server maliciously getting taken over and used for spam. I had to contact the support team, tell them what I was up to, promise I’d be dilligent and then ask them to open the port, which they did. I wasted about 1 – 1.5 weeks trying to unsuccessfully work with port 25 closed, which I was poorly advised to try by their support team, before finally realising it MUST be open and they were subsequently OK about opening it!

So make sure port 25 is open at your hosting provider before proceeding.

Docker Mailserver

The documentation for this package is fantastic. It’s ready to use out of the box with configuration all going into the provided mailserver.env file. We’ll be working with the env file and it’s compose.yaml. So first off, get those 2 files from the repo and recreate them in a local mailserver folder. Also create a docker-data folder in there, which the mail server will use for persistence.

SSL Certs

As mentioned above, the nginx-proxy package handles all my SSL via Let’s Encrypt. To get this to work, you need to add some environment variables to your compose.yaml, see the new environment block below. Docker Mailserver needs to be able to read the various SSL files that nginx-proxy creates, so we also need to map a new volume for the folder that nginx-proxy writes to, that Docker Mailserver will know to read; see the final line under the volume block of docker-compose below.

compose.yaml

As mentioned above, this is largely ready to go out of the box. Here is my slightly edited one (see comments for this blost starting with #DC), with the environment variables for nginx-proxy & Let’s Encrypt, and the volume where SSL certs are stored:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: '2'
services:
  mailserver:
    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    container_name: mailserver
    hostname: mail.example.com
    env_file: mailserver.env
    ports: #DC Removed ports 110 and 143 here, for security
      - "25:25"
      - "465:465"
      - "587:587"
      - "993:993"
    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
      #DC Added this line to map in SSL certs:
      - /var/lib/docker/volumes/nginx-proxy_certs/_data/:/etc/letsencrypt/live/:ro
    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
    #DC Added this section for SSL
    environment:
      - SSL_TYPE=letsencrypt
      - VIRTUAL_HOST=mail.example.com
      - LETSENCRYPT_HOST=mail.example.com

mailserver.env

There are a huge number of options here which are well explained in the file and in the documentation. I’ll detail the ones I thought were important:

1
2
3
4
5
6
7
8
SPOOF_PROTECTION=1
ENABLE_POLICYD_SPF=0 # since I'll be using RSPAMD
ENABLE_OPENDKIM=0 # as above
ENABLE_CLAMAV=1
ENABLE_RSPAMD=1
ENABLE_RSPAMD_REDIS=1
ENABLE_FAIL2BAN=1
SSL_TYPE=letsencrypt

Bring up the server

At this point we’re ready to start the mailserver. This all just runs on the CLI, there’s no GUI until we install Rainloop. So, from within our mailserver folder, we simply do:

1
docker-compose up -d

Creating first accounts

Before setting up all the DNS records, the documentation recommends to create an email account, so let’s do that. We’ll also create a ‘postmaster’ alias for this; it’s standard practice to have a ‘postmaster@’ account, it’s up to you whether or not to have it separate or alias it. We’ll be using the postmaster account in the next section.

1
2
docker exec -it mailserver setup email add hello@example.com
docker exec -it mailserver setup alias add hello@example.com postmaster@example.com

DNS Records

In order to prove you’re sending valid email, i.e. you are who you say you are and you’re not spoofing a service, certain DNS records need to be setup. For this, you’ll need to access your control panel in your DNS provider (usually whoever you registered your domain with).

Once all the below is done, I suggest waiting a day before moving on to the Rainloop section, in order for your DNS changes to propagate around the world.

A Records

First up, let’s setup up some A records and the MX record. Using your server’s IP address, I would suggest setting up A records for:

  • mail.example.com
  • webmail.example.com
  • mta-sts.example.com

For the last of these domains, you also need to configure your primary webserver to also host it (i.e. to respond to requests for mta-sts.example.com), along with your www site. The site doesn’t need to look perfect at this domain, just respond to the initial request.

Next we want to create an MX record. You can leave the Host name blank, use the default TTL (mine’s 1 day), priority 10 and set Result to mail.example.com.

DKIM, DMARC and SPF

These next parts are key for sending email successfully. I’ll roughly outline what to do, but more info can be found at the Docker Mailserver guide for this.

DKIM

For DKIM, as per the guide, you can set this up via

1
docker exec -it mailserver setup config dkim

Assuming you’ve kept the same volume structure from compose.yaml above, this will output files into ./docker-data/dms/config/rspamd/dkim. Have a look at the contents of rsa-2048-mail-example.com.public.dns.txt and copy them to the clipboard. Go back to your Control Panel and create a new TXT record, using mail._domainkey as the Host name value, paste in your copied data to the Result field, use the same TTL as before and save.

DMARC

For DMARC, you just need to create another TXT record. Use _dmarc for Host name. For Result, the guide gives you some defaults, as well as linking to a generator where you can come up with your own. You’ll want to use the postmaster@example.com alias for this: this is how other mailservers will send you reports on issues they’ve encountered with your service. For reference, I went with the following: v=DMARC1;p=quarantine;pct=100;rua=mailto:postmaster@example.com;ruf=mailto:postmaster@example.com;adkim=s;aspf=r

SPF

This is one is simple enough. Simply create another TXT record with an empty Host name and the following for Result: v=spf1 ip4:your.ip.address -all

Other Records

Above, I said to create an A record for mta-sts.example.com. In addition to this, we want to create a TXT record with Host name set to _mta-sts and Result set to v=STSv1; id=yyyymmddxx, where yyyymmddxx is the value of the Serial Number field in your DNS control panel. Once this is done, you need to create a corresponding .well-known/mta-sts.txt file in your primary website’s root folder, i.e. so it’s accessible at https://www.example.com/.well-known/mta-sts.txt and https://mta-sts.example.com/.well-known/mta-sts.txt. Set the contents of this file to:

1
2
3
4
version: STSv1
mode: enforce
mx: mail.example.com
max_age: 86400

Finally, I also have the following TXT record, so I suggest you set it up also: Host name of _smtp._tls and Result of v=TLSRPTv1; rua=mailto:postmaster@example.com.

Reverse IP Address

Once we’re done with all the DNS records, we need to setup a reverse IP lookup. We do this in the admin/control panel of your hosting provider. Each hosting provider is different, but with mine it was pretty straight forward. I found the page I wanted under Network > Public Networks. In here, I added a new entry to the existing table, setting IP Address as my server’s IP and Reverse DNS Name as mail.example.com.

At this point, restart your mailserver docker machine (just to make sure all changes are in effect) and at that point, if all goes well, you’re actually hosting a proper mail server! Now we just want a way to interact with it…

Aside: Connect to nginx-proxy

My nginx-proxy server connects to an internal docker network, to which all sites behind the proxy must connect. Let’s say mine is called DCNET, so once you’ve restarted the image, connect to the network via

1
docker network connect DCNET mailserver

Rainloop

Again we’re going to be using a docker image and a github repo here, wernerfred/docker-rainloop. Above we created a mailserver folder; for Rainloop, I created a rainloop sub-folder in here, but you can create it at the same level as mailserver if you prefer. We’re going to create a second compose.yaml file; you could ultimately merge this with your compose.yaml from above, but that’s beyond the scope of this article. My rainloop instance will run on port 9009.

Here is what I have for my rainloop/compose.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
version: '3'
networks:
  DCNET:
    external: true # name of the network that nginx-proxy connects to
services:
  rainloop:
    image: wernerfred/docker-rainloop:latest
    container_name: docker-rainloop
    restart: always
    volumes:
      - rainloop_data:/rainloop/data
      - ./000-default.conf:/etc/apache2/sites-available/000-default.conf
      - ./ports.conf:/etc/apache2/ports.conf
      - ./apache2.conf:/etc/apache2/apache2.conf
    environment:
      VIRTUAL_HOST: "webmail.example.com"
      VIRTUAL_PORT: "9009"
      LETSENCRYPT_HOST: "webmail.example.com"
    networks:
      - DCNET
volumes:
  rainloop_data:

If you look under volumes, you’ll see 3 local files mapped in to the image. These configure Rainloop’s internal server (running Apache) to work off the correct port, set log formats and more. For simplicity, I’ll just paste the contents of each:

000-default.conf

1
2
3
4
5
6
7
<VirtualHost *:9009>
    ServerAdmin webmaster@localhost
    DocumentRoot /rainloop

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

ports.conf

1
2
3
4
5
6
7
8
Listen 80
Listen 9009
<IfModule ssl_module>
    Listen 443
</IfModule>
<IfModule mod_gnutls.c>
    Listen 443
</IfModule>

apache2.conf

This file is a bit long, so what I suggest is to simply copy the contents of /etc/apache2/sites-available/000-default.conf from inside the rainloop image, save it as 000-default.conf and append the following line:

1
ServerName "webmail.example.com"

Bring up the server and enjoy!

OK, hopefully everything will be configured correctly, so now all you need to do is bring up the server. So, within the rainloop directory you can do

1
docker-compose up -d

then head on over to webmail.example.com and you should see your nice webmail interface! You’ll want to configure it straight away by going to webmail.example.com/?admin, log in using admin / 12345 as the username and password, change your admin password and then set any other configuration you like.

Finally, going back to the root webmail login, you should be able to log in with the account you created above. At this point, you should be up and running, able to send and receive emails. Now, think of what you’re going to spend the €60/year you’re saving on!

Tags: docker email | david | 2nd Apr, 2026 at 13:03pm | No Comments

No Comments

Leave a reply

You must be logged in to post a comment.