LXD eMail, SMTP/IMAP/WebMail with OpenSMTPD, Dovecot, and Roundcube.

Email is one of those conceptually simple things that are a lot more complex in practise – get it wrong and you miss incoming mail, or your mail gets lost or junked, or spammers exploit your server.

This post is intended for technical people who want to run their own personal mail server, and describes the steps required to get a basic server setup that can be run safely and reliably.

The approach I have taken is to run everything in a single LXD container, so that the entire system can be backed up and restored, or even recreated, with little effort.

The steps required are as follows, where step 3 is the focus of this article, but I’ll say a few words on steps 1 and 2.

  1. Register a domain with a reputable registrar.
  2. Get a VPS on a public IP address, and configure reverse DNS.
  3. Create a container on your new VPS to run all email services.

I have since written a bash script that configures the host and the container, and handles pretty much everything covered in this article. You can clone it here: –


Step 1 – Register your domain

I can’t make an informed recommendation about registrars because I’ve really only ever used Gandi (at least since Network Solutions turned bad back in the day). Regardless, I recommend them – they are genuinely technical people, have great DNS management, and an API for automated management.

Step 2 – get a VPS

An entry level VPS is enough to run a complete personal email environment. A minimum of 512MB RAM, and 10GB disk space will allow you about 7GB of Inbox storage. My Google Inbox of many (many!) years is just below 3GB.

For VPS providers, in the past I can recall using Linode, Digital Ocean, Hetzner, who have all been excellent, OVH who I’ve found to be OK, and Virtono who have really cheap offerings, but with unreliable infrastructure (they do seem to be making a sincere effort to offer cheap VPS, which I think is commendable).

I also recently tried Scaleway, who have a good low-end offering (currently 2-Cores 2GB RAM, 20GB HD for €3/month), great UI, and the servers and network seem good. They block outgoing SMTP by default, though it’s possible to have this lifted by contacting support. Whether they would do this for a brand new account with no history, I don’t know.

As I write this, Hetzner would be my VPS choice, since they have reliable infrastructure and a good management interface, yet offer low price VPS.

Scaleway certainly seem good, though enabling SMTP may not generally be as easy as it was for me (I reported a firewall bug to them, so they kind of owed me). Please let me know your experiences. However, the first server I spun up with them got an immediate 10/10 on mail-tester.com – no blacklists at all, and a possible benefit of their default policy to block outgoing SMTP.

In any case, you’d be best to evaluate for yourself. Providers change over time, and one person’s experience could easily differ from someone else’s. Please do share your own recommendations in the comments.

I have tested using Debian Stretch and Buster for the host, and Buster for the container. Debian offers stability and dependability over cutting-edge features, which I think is important for an email server.

Step 3 – Everything else!

Assuming a domain has been created, and you have a VPS with a public IPv4 address, then the following steps can be carried out.

The DNS changes.

Decide on a host name for mail and webmail DNS A record entries. I’ll just refer to these as mail and webmail. If your domain is example.com, then your hosts will be mail.example.com and webmail.example.com, both resolving to your public IPv4 address.

You will need an MX record for your domain, which points to mail.example.com, and I also include an A record for the root domain itself (e.g. example.com resolves to If you want to serve content from your container at www.example.com, then add a www entry too.

For reference, these would look like this in an zone file.

@        600  IN  A
@        600  IN  MX  10 mail.example.com.
webmail  600  IN  A
mail     600  IN  A
www      600  IN  A ; optional

For reverse DNS configuration, this has to be done with your VPS provider, because it is their IP network that is the route to resolution. Most allow this to be configured in their management interface, but you may need to ask them to add this manually via a support ticket.

Your public IPv4 address, should resolve to mail.example.com.

Prepare the host

The VPS host will really only be used to run the container. We will install LXD via snapd, so lets get that done.

apt-get install snapd
snap install lxd

adduser kevin       # this lets us move out of root
adduser kevin lxd   # permission to use lxd
adduser kevin sudo  # always good to be a sudoer

su - kevin
lxd init  # Use defaults, except 10GB storage & IPv6 'none'

We’re now ready to create a container, which we’ll name ‘mailc’: –

 lxc init 'images:debian/10' mailc

We’re going to hardcode its IP address since, as I understand it, this is necessary to allow the use of NAT rather than plain proxying to expose the external ports. Without this, the source address of SMTP traffic can appear local, effectively leaving us with an open-relay because the SMTP server is configured to handle local traffic differently.

# Get our bridge network address...
lxc network get lxdbr0 ipv4.address
# ... returns something like

# Hardcode our container's IP to a specific address
lxc network attach lxdbr0 mailc eth0 eth0
lxc config device set mailc eth0 ipv4.address

# Give our container a meaningful hostname
lxc start mailc
echo 'mail.example.com'|lxc exec mailc tee /etc/hostname
lxc restart mailc

lxc config device add mailc smtp proxy listen=tcp: \
    connect=tcp: nat=true
lxc config device add mailc esmtp proxy listen=tcp: \
    connect=tcp: nat=true 
lxc config device add mailc http proxy listen=tcp: \
    connect=tcp: nat=true
lxc config device add mailc https proxy listen=tcp: \
    connect=tcp: nat=true

Jump onto the container

We’re now in a position to run a shell in our container to configure it. Most of the remaining instructions are performed in the container, so it’s important to make sure you’re actually running there.

lxc exec mailc bash   # We're going into the container here.

Install the first set of packages to get us to the point of having an SMTP server.

apt-get install procps curl cron opensmtpd apache2 dkimproxy

Do the DKIM thing

Now that dkimproxy has been installed, we can configure it and generate the entry needed for our domain’s DNS.

# Without this dkimproxy can't read the key!
chgrp dkimproxy /var/lib/dkimproxy/private.key

Now we edit /etc/default/dkimproxy and ensure that: –


The last line is a bit of a hack to ensure the process is run with the correct domain – otherwise it uses the output of $(hostname -d) that, perhaps due to the way containers initialise, can return 'localdomain' when run on startup (resulting, of course, in us signing for the wrong domain).

We also need to edit /etc/dkimproxy/dkimproxy_out.conf and ensure the following values: –

selector  mainsel
domain    example.com

We’re now ready to add our domain key to our DNS zone. This has a specific format, so we need to extract the correct text from the RSA public.key file that was auto-generated when we installed the dkimproxy package.

# Extract the key data from the public key file.
DKIM_PUB=$(for line in \
   $(grep -v 'PUBLIC KEY' /var/lib/dkimproxy/public.key); do \
     echo -n ${line}; done \
# Form the full entry for our TXT record
echo "v=DKIM1; k=rsa; p=${DKIM_PUB}"

The text generated from the code above should be entered in your DNS as a TXT record with name mainsel._domainkey, ideally with a low TTL value (I use 600 seconds) because if you ever regenerate it, you don’t want the old value hanging around too long.

Next, the SPF record

The SPF record is added as a TXT record in the root of the domain (@ in a zone file), meaning it has no name. The public IPv4 address of your VPS should be specified in place of

 v=spf1 a mx ip4: -all 

Finally, the DMARC record

This is a TXT record with name _dmarc that contains the following text (all one line, in case your browser line-breaks it), with example.com replaced with your registered domain.


In case you’re not familiar with these records, an SPF record declares what servers are allowed to send email from someone @your.domain. It prevents some random server from sending email purportedly coming from you as a sender, because the receiving SMTP server can check via your DNS whether it should accept it (though there’s no obligation for the server to reject it).

A DKIM record declares a public key that can be used by any email receiver or reader to verify that the message was sent by authorised software (and not, for example, by some malicious code that has managed to get onto your SMTP server, which SPF would miss). It also happens to verify that the email hasn’t been tampered with in transit, such as spam adverts getting injected into your message.

The DMARC record really just declares how you would like failures of such authorisations to be handled (reject or ignore, etc.) and allows you to specify where to send failure reports.

Get a LetsEncrypt key for SMTP/TLS and WebMail

This bit is now a bit of a breeze, thanks to Neil Pang’s acme.sh script, which has to be one of the best examples of bash scripting that I’ve ever seen.

curl -s https://get.acme.sh | bash
# Issue your certificate, be sure you agree with the LetsEncrypt
# Subscriber Agreement before doing this.
# https://letsencrypt.org/repository/
acme.sh --issue \
    -d mail.example.com -d webmail.example.com \
    -d example.com -d www.example.com \
    -w /var/www/html 

You may choose to omit www.example.com if you didn’t define it in your DNS.

We can now go on to install the certificate in a directory that will be used later by OpenSMTPD and Apache.

mkdir -p /etc/letsencrypt/acme.sh
acme.sh --install-cert \
  -d mail.example.com -d webmail.example.com \
  -d example.com -d www.example.com \
  --cert-file      /etc/letsencrypt/acme.sh/cert-example.com.pem \
  --key-file       /etc/letsencrypt/acme.sh/cert-example.com.key \
  --fullchain-file /etc/letsencrypt/acme.sh/fullchain-example.com.pem \
  --reloadcmd     "service apache2 force-reload"

OpenSMTPD – /etc/smtpd.conf

Configuring OpenSMTPD is really easy, as SMTP servers go. Most of the configuration options below are well documented, so I’ll only explain the lines that relate to DKIMProxy, a process that listens on a TCP port, signs any email that it receives, and then sends it on via another port.

My Email -> OpenSMTPD -> DKIMProxy -> OpenSMTPD -> Recipient

The accept line with relay via smtp:// defines a rule that sends local-to-external mail via local port This is the port that DKIMProxy is listening on, to accept email for signing.

DKIMProxy then signs our email and delivers it back to OpenSMTPD – the line with listen on port 10029 with the term tag DKIM_OUT, defines that port to accept our emails back, and we ‘tag’ the session as having come out of DKIMProxy.

The line that begins accept tagged DKIM_OUT states that any email from a session tagged DKIM_OUT can be relayed (e.g. delivered externally).

The only other line I want to explain is limit mta inet4. There are VPS providers that don’t provide an easy interface for configuring IPv6 reverse DNS. If you send using IPv6 without having reverse DNS then there’s a good chance your mail will be rejected. For example, this is a strict GMail policy – it helps them avoid mail from botnets, etc.

If you have IPv6 reverse DNS configured, you should be able to remove that line, and have the MTA use whatever IP stack it can.

Use OpenSMTPD as a SmartHost

We can use our external port 587 as a smart-host by authenticating, after which our session is considered ‘local’ by the remaining rules in our configuration.

For flexibility, rather than relying on the standard PAM authentication, I am using a separate passwd file. In this case, the password has to be encrypted with smtpctl encrypt mypassword123, the resulting string added to the file as follows: –

root@vps:~# smtpctl encrypt mypassword123

Edit file /etc/smtpd_passwd so it contains: -

mysmtplogin  $6$sBY.d2oDx695J9jQ$tI9ObEY5mBvooH00kMEDfQFFePFXkBFhAPSyytVhAhj/9zn49W0Egj51og4LUZpzedN5CmEfj3kGYdeE2vQsk. 

The credentials that I would use to configure mail clients, web applications, etc. is therefore user mysmtplogin, password password123, allowing me to send mail from my domain with all the reassurance that SPF, DKIM, and DMARC give to the recipient’s server.

# /etc/smtpd.conf - the OpenSMTPD configuration
# Define cert/key to use by name later
pki letsencrypt certificate "/etc/letsencrypt/acme.sh/fullchain-example.com.pem"
pki letsencrypt key "/etc/letsencrypt/acme.sh/cert-example.com.key"

table aliases file:/etc/aliases
table users file:/etc/smtpd_passwd
# Define our listening ports, internal and public
listen on port 25 hostname mail.example.com tls pki letsencrypt
listen on port 587 hostname mail.example.com tls-require pki letsencrypt auth mask-source
listen on eth0 port 25 hostname mail.example.com tls pki letsencrypt
listen on eth0 port 587 hostname mail.example.com tls-require pki letsencrypt auth <users> mask-source
listen on lo port 10029 tag DKIM_OUT

# Force IPv4 for simplicity.
limit mta inet4

# Define our rules, evaluated in order of definition
accept from any for domain "example.com" alias <aliases> deliver to maildir
accept for local alias <aliases> deliver to maildir
accept tagged DKIM_OUT for any relay
accept from local for any relay via smtp://

Note: when the smtpd.conf man pages show that a table name is referenced (e.g. <aliases> and <users> above), the angled brackets (< >) should actually be included. I got stung by this – it took me over an hour to see the obvious!

Default PAM authentication: if eth0 port 587 above is configured simply with auth, rather than auth <users>, then you can authenticate to the sever with your Unix username and password, same as for a webmail login. However, I prefer to keep smarthost authentication separate from my Unix account – smarthost authentication is likely to be used for scripts, apps, etc., so if a script or app goes rogue, then I don’t compromise my main email (or Unix) account, and can simply change that smarthost password.

Roundcube WebMail via Dovecot IMAP

We will install Dovecot and Roundcube. If the installation asks whether to use dbconfig, answer yes.

 apt-get install dovecot-imapd roundcube roundcube-sqlite3

We will then configure Dovecot to look for email in Maildir format. Edit /etc/dovecot/conf.d/10-mail.conf and change the mail_location entry from mbox to the following: –

 mail_location = maildir:~/Maildir

To configure Roundcube we edit /etc/roundcube/config.inc.php and configure the following values to define how to connect to the IMAP server, and to prevent trying to authenticate our outgoing SMTP connections (we don’t use SMTP authentication since we’re connecting locally).

$config['default_host'] = 'localhost:143';
// $config['smtp_user'] = ''; // comment out
// $config['smtp_pass'] = ''; // comment out

We also need to edit /etc/roundcube/defaults.inc.php to hardcode the mail domain for our default identity (e.g. @example.com).

 $config['mail_domain'] = 'example.com'; 

Configure Apache

We will set up Apache to serve Roundcube only via HTTPS. The following is a condensed version of the Apache default SSL site configuration, with values appropriate for our setup. Write it to /etc/apache2/sites-available/roundcube.conf.

<IfModule mod_ssl.c>
  <VirtualHost *:443>
    ServerAdmin webmaster@example.com
    ServerName  webmail.example.com
    DocumentRoot /var/lib/roundcube/
    ErrorLog ${APACHE_LOG_DIR}/roundcube-error.log
    CustomLog ${APACHE_LOG_DIR}/roundcube-access.log combined
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/acme.sh/cert-example.com.pem
    SSLCertificateKeyFile /etc/letsencrypt/acme.sh/cert-example.com.key
    SSLCertificateChainFile /etc/letsencrypt/acme.sh/fullchain-example.com.pem 
    <FilesMatch "\.(cgi|shtml|phtml|php)$">
      SSLOptions +StdEnvVars
    <Directory /usr/lib/cgi-bin>
      SSLOptions +StdEnvVars

We can now enable the site and the ssl mod with the following two commands and reboot the container: –

a2enmod ssl
a2ensite roundcube

We should now have a working email system. It’s important now to verify the configuration at this stage. I can’t stress this enough. There are tools listed in the references that will help with this. The most important check, in terms of social responsibility, is to ensure you’re not running an open-relay (a mail server that will accept mail for anyone, from anyone).

I managed to leave an open relay for a few hours while developing this – traffic proxied to the container from the external interface was appearing as local, and of course we’re more lenient towards local traffic! The SMTP server rules were all correct, but the packets themselves were not – NAT was required.

What was striking was just how quickly it was discovered and exploited. Interestingly, despite the IP hitting around 5 blacklists, GMail was still happy to deliver my mail. All but one blacklist got cleared within an hour, with the last auto-clearing after about a week. Not a disaster, but still embarrassing.


The following are links to useful sites I found while putting this together.

Next Steps

Probably next to add to this is spam-checking of incoming mail (spamassassin), and there’s also firewall configuration to consider (ufw), though on a vanilla Debian Buster host, likely only SSH (port 22) will be exposed, over and above ports 80, 443, 25, and 587 for the container.

There’s some info on using spamassassin with OpenSMTPD by Eric Radman that looks useful. The approach is similar to DKIM signing (i.e. diverting the message through a proxy). He also discusses, in other posts, setup of a mail server generally, so there’s more there worth reading.

The IMAP server needs to be exposed so that email can be read on external devices (e.g. a phone’s email client), but I have held off because of a zero-day reported against Dovecot recently.

There’s also the longer-term issue of Maildir backups (easily done with some combination of tar/gzip/rsync), container backups, and container migration, the latter being important because the reason I’m using a container is to avoid being too dependent on an ISP or VPS provider – it’s easier to move a container than it is to configure the whole system on a new server.

Leave a Reply

Your email address will not be published. Required fields are marked *