In 2016 I wrote how to set up e-mail with Docker. Lots of you read it (over 15,000 views), and the comments taught me a few things. This autumn I moved my sites to a new server and set my @svenkauber.com and @infohaiku.com addresses up again, this time with everything I have learned since. Here is the 2026 version.
Everything below is in a small repository you can clone: docker-mail-stack. Its README has every command; this post explains the choices.
What changed since 2016
- The image grew up. Thomas Vial's tvial/docker-mailserver is now docker-mailserver, maintained by a team, at version 16 (Debian 13, Postfix 3.10, Dovecot 2.4).
- rspamd instead of SpamAssassin, OpenDKIM and postgrey. One program filters spam, greylists, signs outgoing mail with DKIM and checks SPF and DMARC. The greylisting I added by hand in 2016 became my pull request #495 to docker-mailserver in 2017; today rspamd does it out of the box.
- No more changes inside the running container. Erik pointed this out in the comments in 2018, and he was right: what you change with docker exec is gone after the next image update. Now all settings live in files next to compose.yaml and are mounted into the container.
- Webmail. Roundcube runs as a second container, with filters and vacation replies.
- Updates and backups are part of the setup, not an afterthought.
- No ClamAV. The virus scanner alone needs about 1 GB of RAM. On a 2 GB server shared with websites, both mail containers together use about 400 MB.
Before you start
- A server with Docker and a public IPv4 address. Its reverse DNS (PTR) should match your domain: on DigitalOcean, naming the droplet after the domain sets it.
- Outgoing port 25 must be open. Some cloud providers block it on new accounts, and most home internet connections block it too, so test from the server.
- nginx and certbot on the server, for the certificate and the webmail page.
Setting it up
- A host name for mail. Add an A record mail.yourdomain.com pointing to the server, then get a certificate:
The deploy hook restarts the mail server whenever the certificate is renewed.sudo certbot certonly --nginx -d mail.yourdomain.com \ --deploy-hook "docker restart mailserver" - Clone and configure.
git clone https://github.com/adrl-sven-kauber/docker-mail-stack.git ~/mail cd ~/mail cp .env.example .env # host name, postmaster address, Roundcube key docker compose up -d - Mailboxes and aliases.
One server can serve several domains: I have three mailboxes and four aliases on two domains.docker exec mailserver setup email add you@yourdomain.com 'a-long-password' docker exec mailserver setup alias add info@yourdomain.com \ you@yourdomain.com - DKIM keys, one per domain:
The script prints the DNS record for each domain. Why a script? docker-mailserver's own command, given two domains at once, makes one broken key named after both of them, and it writes the signing settings for the first domain only. That one surprised me.tools/add-dkim.sh yourdomain.com otherdomain.com - DNS records for every domain:
- MX: 10 mail.yourdomain.com, and delete any old MX records (forwarding services, your registrar's e-mail forwarding)
- SPF (TXT on the domain): v=spf1 a:mail.yourdomain.com mx -all
- DKIM (TXT on mail._domainkey): the value from step 4
- DMARC (TXT on _dmarc): v=DMARC1; p=none
- Webmail. Roundcube listens only on the server itself; nginx serves it at https://mail.yourdomain.com. The repository has the nginx config.
- Test. Send yourself a mail at Gmail, open it and choose "Show original". You want SPF: PASS, DKIM: PASS, DMARC: PASS. Mine passed on the first try, and still landed in Spam: a new server has no reputation yet. Mark it "Not spam", and it gets better.
In your mail app: IMAP mail.yourdomain.com, port 993, SSL/TLS; SMTP the same host, port 587 (STARTTLS) or 465 (SSL/TLS); user name is the full address.
Updates and backups
The images are pinned to a version line: docker-mailserver 16 and Roundcube 1.7. Every Sunday night update.sh pulls the newest release within those lines and restarts what changed. A jump to a new major version stays a manual step: version 16 changed the Dovecot configuration format, and I read its changelog before upgrading.
Backups run every night in two steps. On the server, backup.sh copies mailboxes, accounts, DKIM keys and the webmail database to a folder. Then another machine (mine is at home) pulls that folder over SSH into dated snapshots. Files that did not change are shared between snapshots, so 30 days of history cost little more than one copy.
Bringing old mail back
My mail from 2016 to 2018 was still on a copy of an old server: three mailboxes, about 11,000 messages. Maildir folders copy over as they are. One surprise: every message showed March 2024 as its date. The file times had been reset when the old server was moved in 2024, and IMAP shows a message's file time as the date it arrived. tools/fix-maildir-dates.py sets every file's time from the message's own Date: line, and the mail sorted correctly again.
Things worth knowing
- Docker ports bypass ufw. Erik's doubt about ufw in 2018 had a point: a port that Docker publishes is open even when ufw says it is closed. The mail ports are meant to be public; the webmail port is bound to 127.0.0.1 only.
- Testing SPF from inside the container fails, because there the mail host name points to the container itself. Test from outside.
- fail2ban and webmail. All webmail logins come from Roundcube's container. If fail2ban banned that address after a few mistyped passwords, nobody could log in. The setup tells fail2ban to ignore it, and Roundcube limits failed logins itself.
- DNS panels can lag. My registrar's panel already showed the new MX record while its name servers still served the old zone for hours. dig SOA yourdomain.com @nameserver shows which version is live.
Questions, or a better way to do something? The comments are open, as they were in 2016.

