Post

Deploy a Gmail-like email server in 30 (ish) minutes

A recent HN discussion got me thinking - why don’t I try and run my own email infrastructure? Some of the main reasons I hadn’t previously tried might come naturally:

  • Email servers are a pain to set up
  • They’re open to attack
  • Hosted email is fairly inexpensive and easy to set up

I recently expanded to having three different domains for email - one for general web logins & CVs, one I give to just family & friends, and the last for my company Jammed. To make things worse, each of these domains is hosted by different providers, so consolidating them all to use one email server would be a fairly good reason to bother.

Well, given I had a Christmas Eve to myself, I decided to try… and it was actually quite a bit easier than I thought it would be.

I’m a bit of a privacy geek, but it wasn’t the driving force behind me owning the infra - but still the thought of all of my business email touching a Google server was always a bit of a sting.

From the off, I had some reasonable requirements:

  • It had to use IMAP, SSL/TLS
  • I had to be able to use mulltiple email accounts with the same server
  • It kinda just had to look after itself once created
  • Be reasonably cheap, but not any cheaper than my current three hosted email subsciptions

At first I found a few github projects that were stale, but eventually came across the excellent Docker Mailserver which is a simple Docker Compose container setup that runs a fully fledged mail server.

The blog article is the setup to make Docker Mailserver act like a Gmail server - if you don’t really care about Archive/Junk/All Mail working, you can just use the default setup and be reasonably happy. I’m sure you could go further with this setup, with Kubernetes, but I’m not going to bother as it’s overkill for a single email server.

For this you need

  • A Digital Ocean (or similar) account
  • A domain name to host it on
  • A DNS setup to point the domain to the server (I use Cloudflare)

Step I: Digital Ocean VM

2GB ram Droplet

I spun up a small Digital Ocean droplet with using the ‘Docker’ marketplace image. Docker Mailserver needs at least 2GB of RAM to run - this can be less if you disable virus scanning (which loads the full signature database in memory at boot).

I set mine up in the London region, but anywhere that’s close to you physically is fine.

Attach a volume

This is the most important part - you attach a block storage volume to the droplet, so you have the option later of scaling up the storage, independently of the droplet size. The volume is attached to the droplet at boot, and is then mounted on the host machine.

I added a 50GB volume to mine, because it’s easy to scale later.

Enable backups

This should be pretty self-explanatory - email is pretty important data, so it’s important to keep it backed up.

Step II: ssh root@droplet-ip

Login to your droplet, and run the following commands:

1
2
3
4
5
6
7
8
9
mkdir mail
cd mail
DMS_GITHUB_URL='https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master'
wget "${DMS_GITHUB_URL}/docker-compose.yml"
wget "${DMS_GITHUB_URL}/mailserver.env"
wget "${DMS_GITHUB_URL}/setup.sh"

chmod a+x ./setup.sh
./setup.sh help

setup.sh is the main script that configures the mail server.

Step III: Mount the block volume

Head to ‘Volumes’ in the Digital Ocean dashboard, and attach the volume to your droplet (unless it already has done so). Next, find the ‘configure your volume’ section, and follow the instructions to mount the volume.

After this point, the droplet will always mount the volume at boot, and the volume will be mounted on the host machine for us to use as the data store for email, config files, etc.

Step IV: Add the mount points & change domain name in docker-compose.yml

Domain

First, at the top of the file, add your full domain name in as the hostname variable.

1
2
3
...
    hostname: mail.example.com
...

Volumes

Now we have a persistent block volume, we can change the mount points in the docker-compose.yml file to point to the volume.

Mine looks like this:

1
2
3
4
5
6
volumes:
    - /mnt/volume_lon1_01/mail-data/:/var/mail/
    - /mnt/volume_lon1_01/mail-state/:/var/mail-state/
    - /mnt/volume_lon1_01/mail-logs/:/var/log/mail/
    - /mnt/volume_lon1_01/config/:/tmp/docker-mailserver/
    - /etc/localtime:/etc/localtime:ro

mnt/volume_lon1_01 is my Digital Ocean block volume, and setting these will mount the Docker volumes on the host machine in the right places.

Step V: Configure letsencrypt

Follow the instructions to setup Certbot to automatically provision and renew the certificate for the domain you want to use. For Cloudflare, you can use their DNS provisioning plugin using an API key that verifies the domain you want to use without needing to set up a nginx server to serve the challenge.

Make sure you provision a certificate for the subdomain you are using, and it needs to exactly match the host you use for the mailserver. For example, if you are using mail.example.com as the host, you need to provision a certificate for mail.example.com and not example.com or you’ll get IMAP TLS auth errors when you try to connect to the server.

You run certbot on the droplet itself, not in Docker. Once the certificate is provisioned, you then just add the /etc/letsencrypt volume into the docker-compose.yml file so that the host machine certificates are available to the containers.

1
2
3
4
5
6
7
volumes:
    - /mnt/volume_lon1_01/mail-data/:/var/mail/
    - /mnt/volume_lon1_01/mail-state/:/var/mail-state/
    - /mnt/volume_lon1_01/mail-logs/:/var/log/mail/
    - /mnt/volume_lon1_01/config/:/tmp/docker-mailserver/
    - /etc/letsencrypt/:/etc/letsencrypt/
    - /etc/localtime:/etc/localtime:ro

Step VI: Configure IMAP folders

By default, docker-mailserver only provides ‘Inbox’, ‘Sent’, ‘Drafts’ folders for IMAP. These are termed ‘Special folders’, and most clients now support ones like ‘Archive’ and ‘Junk’ as well. I really like this feature, and I’m not sure why it’s not in the default, but hey ho! I’ll add it:

To make changes here, we need to configure dovecot to add these new folders to the IMAP server.

First, grab the dovecot.conf file from the master branch of the docker-mailserver repo.

1
2
cd /root/mail
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/target/dovecot/15-mailboxes.conf

Now open it and edit it to uncomment the lines about ‘Archive’ & ‘Junk’. I’ll leave this part as your choice. Once you’ve made your changes, modify the docker-compose.yml file to mount the new .conf file for dovecot.

1
2
3
4
5
6
7
8
volumes:
    - /mnt/volume_lon1_01/mail-data/:/var/mail/
    - /mnt/volume_lon1_01/mail-state/:/var/mail-state/
    - /mnt/volume_lon1_01/mail-logs/:/var/log/mail/
    - /mnt/volume_lon1_01/config/:/tmp/docker-mailserver/
    - /root/mail/15-mailboxes.conf:/etc/dovecot/conf.d/15-mailboxes.conf:ro
    - /etc/letsencrypt/:/etc/letsencrypt/:ro
    - /etc/localtime:/etc/localtime:ro

Step VII: Modify the mailserver.env file

A few things we need to do here:

  • Make sure ClamAV is enabled ENABLE_CLAMAV=1
  • Make sure fail2ban is enabled ENABLE_FAIL2BAN=1
  • Make sure letsecrypt is enable SSL_TYPE=letsencrypt

Otherwise read

Step VIII: Create the first email account

We can’t start the docker compose yet, as we don’t yet have an initial email account. Instead, spin up a copy of docker-mailserver with the following command:

1
docker run --rm -v "/mnt/volume_lon1_01/config/:/tmp/docker-mailserver/" docker.io/mailserver/docker-mailserver setup email add <user@domain>

Change /mnt/volume_lon1_01/config for your block volume mount point. You’ll be prompted to create a password for the email account.

Step IX: Boot the containers in the foreground

One mistake I made initially was to start the containers in the background. This is because I was using the docker-compose up -d mailserver command to start the containers. This is the recommended way to start containers once everything is ready, but to make sure everything is working, use docker-compose up command to start the containers in the foreground.

Any letencrypt errors may be due to DNS not propogating, or the /etc/letsencrypt/ directory not being mounted correctly.

Step X: Configure DKIM

DKIM is the way to sign emails with your domain’s public key. It’s really important this step is done, as it is one big way to prevent spam from being sent to your email account and a huge signal that this mail server is legit.

1
./setup.sh config dkim

Add these as a TXT DNS record to the domain you want to use.

Step XI: Test IMAP & SMTP

Create a new mailbox locally to your machine, and use the IMAP server, email, password you just created to connect to the server. You should see a list of folders, and you should be able to send an email to the mailbox. I found the OSX’s Mail has a Connection Doctor that will tell you if you’re connected to the server, and if not, what error actually occurred. Test send, receive, and delete emails.

Step XII: Add other accounts, DMARC

A big one for me was to add a second email account. I’m using a different domain, so I need to create a new mailbox for that domain. I’ll do this by running the following command:

1
2
./setup.sh email add [email protected]
./setup.sh email add [email protected]

DMARC is the method to allow spam and abuse reports to be sent and handled by the email server. I won’t go into it here, but Docker Webserver handles this for you transparently - you just need to add the DNS TXT record to your domain & the email server will do the rest.

Conclusion

It was easier than I thought to create a mail server that works as well as Gmail’s, and now really happy that I’m one of those weirdos that hosts their own mail server.

Andy Callaghan makes Jammed - booking software for music rehearsal studios and recording studios

This post is licensed under CC BY 4.0 by the author.