Streaming & Messaging AWS

docker-mailserver on AWS User Guide

| Product: docker-mailserver on AWS

Overview

This guide covers the deployment and configuration of docker-mailserver on AWS using cloudimg's preconfigured Amazon Machine Image. docker-mailserver (DMS) is a production ready, full stack mail server: a Postfix MTA for sending and receiving, Dovecot for IMAP mailbox access, Rspamd for spam scoring, DKIM signing and verification, SPF and DMARC, and Fail2ban for brute force protection, all configured through a single environment file and a plain file based account store. There is no database to run and no web console to secure.

The cloudimg image ships the free and open source, MIT licensed docker-mailserver, run the officially supported way as the upstream container pinned by image digest, on the five standard mail ports. Mail storage lives on a dedicated, independently resizable EBS data volume. Nothing ships with a known credential: the image contains no mailbox at all, and a unique postmaster password and a unique self signed TLS certificate are generated on each instance at first boot, before the server is reachable. Backed by 24/7 cloudimg support.

docker-mailserver is a trademark of its respective owner. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by The Docker Mailserver Organization. It ships the free and open source MIT licensed self hosted software, unmodified.

The docker and docker-mailserver services both active, the digest pinned mailserver container running and healthy, the five published mail ports 25 SMTP, 143 IMAP4 STARTTLS, 465 ESMTP implicit TLS, 587 ESMTP STARTTLS and 993 IMAP4 implicit TLS all listening on 0.0.0.0, and the dedicated ext4 mail data volume mounted at /var/lib/docker-mailserver

Before you start: AWS throttles outbound port 25

This is the single most important thing to know before you deploy a mail server on AWS, so it is stated first rather than buried.

AWS throttles outbound TCP port 25 from EC2 instances by default, to protect the reputation of its address space. This affects sending directly to recipient mail servers. It does not affect anything inbound.

To send mail directly to recipient mail servers on port 25 you must:

  1. Complete the AWS "Request to remove email sending limitations" form for your account and region, and
  2. Set up reverse DNS (a PTR record) for the sending IP. AWS provisions reverse DNS for Elastic IP addresses through the same request.

Until that request is granted, direct-to-MX delivery on port 25 will time out. The supported and recommended path in the meantime — and the simplest one for most deployments — is to relay outbound mail through an authenticated smarthost on port 587, which AWS does not throttle.

What this means in practice for this image:

Works out of the box
Receiving mail on port 25 from other mail servers Yes — inbound is completely unaffected
IMAP mailbox access on 143 and 993 Yes
Authenticated submission from your mail clients on 587 and 465 Yes
Sending by connecting directly to recipient MX servers on port 25 Only after AWS removes the outbound 25 limit and reverse DNS is set
Sending through an authenticated smarthost relay on port 587 Yes — the recommended path

So as shipped this is a fully functional receiving and IMAP mail server, and for sending you either request the port 25 removal or configure a relay. This is not a limitation of docker-mailserver or of this image — it is a platform default that applies to every EC2 based mail server. The image is configured relay first for exactly this reason, and RELAY_PORT defaults to 587 rather than upstream's default of 25, which would silently time out.

The screenshot below is this measurement taken on a real EC2 instance, not a claim from documentation:

Outbound SMTP measured from the EC2 instance itself, showing tcp/25 to a recipient MX BLOCKED_OR_UNREACHABLE while tcp/587 and tcp/465 to a relay provider are both REACHABLE, the dms-set-relay.sh command to configure an authenticated smarthost, and Fail2ban jail status for the custom, dovecot and postfix jails guarding the public mail ports

What is included:

  • docker-mailserver 15.1.0 (the MIT licensed full stack mail server), pinned by image digest
  • Postfix 3.7.11 (MTA), Dovecot 2.3.19.1 (IMAP), Rspamd 3.12.1 (spam, DKIM, SPF, DMARC) and Fail2ban 1.1.0
  • The five standard mail ports published: 25 SMTP, 143 IMAP4 STARTTLS, 465 ESMTP implicit TLS, 587 ESMTP STARTTLS, 993 IMAP4 implicit TLS
  • Fail2ban enabled with the NET_ADMIN capability so bans are actually enforced, guarding the internet facing ports
  • A dedicated 64 GiB ext4 EBS data volume at /var/lib/docker-mailserver holding the Maildir, mail state, mail logs and configuration, independently resizable
  • A unique self signed TLS certificate and a unique postmaster mailbox password generated per instance on first boot, never baked into the image
  • No mailbox and no credential in the shipped image at all
  • ClamAV present but disabled by default, enabled with a single command when you have the RAM for it
  • Bundled component licence notices and a GPL v2 written offer for source, shipped in the image
  • docker-mailserver.service and docker-mailserver-firstboot.service as systemd units, enabled and active on boot
  • Ubuntu 24.04 LTS base with latest security patches applied at build time

Prerequisites

  • An AWS account where you can launch EC2 instances
  • IAM permissions to launch instances, create security groups and subscribe to AWS Marketplace products
  • A key pair for SSH access, and a domain name you control with access to its DNS records
  • An authenticated SMTP relay account if you need to send mail before the AWS port 25 limit is removed (see the sending section)

Recommended instance type: m5.large (2 vCPU, 8 GB RAM). The shipped stack (Postfix, Dovecot, Rspamd with its Redis, Fail2ban) runs in roughly 1 to 1.5 GB, so a smaller instance is workable for evaluation. 8 GB is recommended because it leaves genuine headroom for mailbox volume and for enabling ClamAV, which upstream documents as needing around 3 GB on its own. For high mail volume use a larger instance type.

Security group inbound rules. Allow 22/tcp from your management network only. The mail ports must be reachable from the networks that use them:

Port Purpose Who needs it
25/tcp SMTP — inbound mail from other mail servers The internet, if you receive mail from outside
587/tcp Submission, STARTTLS Your users' mail clients
465/tcp Submission, implicit TLS Your users' mail clients
143/tcp IMAP4, STARTTLS Your users' mail clients
993/tcp IMAP4, implicit TLS Your users' mail clients

Port 25 is the only one that genuinely needs to be open to the whole internet, and only if you want to receive mail from arbitrary senders. If this server is only for sending through a relay and reading over IMAP, leave 25 closed to the internet.

Connecting to your instance

SSH in as the default login user for the operating system variant you launched:

OS variant SSH login user Example
Ubuntu 24.04 ubuntu ssh -i your-key.pem ubuntu@<instance-public-ip>

There is no web console — docker-mailserver is administered over SSH and by standard mail clients. Everything below runs from an SSH session on the instance.

Deploying from the AWS Marketplace

Subscribe to the listing in AWS Marketplace and launch it from the console, or from the AWS CLI. Create a security group that opens SSH to your management network and the mail ports as needed:

aws ec2 run-instances \
  --image-id <ami-id-from-marketplace> \
  --instance-type m5.large \
  --key-name your-key \
  --security-group-ids sg-xxxxxxxx \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=my-mailserver}]'

Open the mail ports on the security group (SSH restricted to your management CIDR):

aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 25 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 587 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 993 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 22 --cidr <your-mgmt-cidr>

For a stable sending identity and reverse DNS, allocate an Elastic IP and associate it with the instance.

First boot

On the first boot of each instance, docker-mailserver-firstboot.service derives this instance's mail hostname, generates a TLS certificate for it, generates a random postmaster password, starts the mail server and records everything in a root only file. It runs once and takes about 30 seconds.

Check that the services are up:

systemctl is-active docker.service docker-mailserver.service

Expected output:

active
active

Confirm the container is running and pinned by digest:

sudo docker ps --format '{{.Names}}  {{.Image}}  {{.Status}}'
cat /opt/docker-mailserver/IMAGE-DIGEST.txt

The digest recorded there is the exact image this instance runs:

ghcr.io/docker-mailserver/docker-mailserver@sha256:af51b15dd3fc72153c0e90eb7692bb5e3a463212d87959a80fa7aa89b617d44a

Confirm all five mail ports are listening:

ss -lntH '( sport = :25 or sport = :143 or sport = :465 or sport = :587 or sport = :993 )' | awk '{print $4}' | sort -u

Per instance credentials

Everything unique to this instance is written to /root/docker-mailserver-credentials.txt, mode 0600, owned by root:

sudo cat /root/docker-mailserver-credentials.txt

The per instance credentials file at mode 0600 root root showing the postmaster address, the mailbox password, the mail host, mail domain and public address; the single per instance mailbox listed by setup email list; the account store holding a SHA512-CRYPT hash rather than a plaintext password; and the per instance TLS certificate subjectAltName carrying this instance's own hostname

About the one mailbox. docker-mailserver will not start Dovecot with zero accounts — it warns and shuts the container down after 120 seconds. So exactly one mailbox is created at first boot: the postmaster@ address, which every mail server is required by RFC 5321 to accept anyway. Its password is generated randomly on your instance, at first boot, and is not present in the image, in any configuration file, or known to cloudimg. The shipped image contains no mailbox at all; first boot asserts this and refuses to proceed if it finds one.

Change the postmaster password whenever you like:

sudo docker exec -ti mailserver setup email update postmaster@<your-domain>

Verifying the server actually works

The image ships a self test that proves real mail flow end to end rather than just checking that a port is open. It creates nothing and leaves nothing behind:

sudo /usr/local/sbin/dms-selftest.sh

It submits a message on port 587 with STARTTLS and SMTP authentication, polls IMAP on 993 until the message is genuinely delivered, fetches it and asserts the exact content, then removes it. It also runs three negative controls that must each be refused: an unauthenticated relay attempt to an external domain on port 25, a submission with a wrong password, and an IMAP login with a wrong password.

The dms-selftest.sh output showing exactly one per instance postmaster mailbox with no leaked account, a message submitted on 587 with STARTTLS and SMTP AUTH, the message retrieved over IMAP 993 with the exact nonce matched and the probe removed, the open-relay attempt rejected with 554 5.7.1 Relay access denied, both the wrong submission password and wrong IMAP password rejected, and the final SELFTEST_OK line

The open relay check is the important one. An open relay is a mail server that will forward mail to arbitrary external domains for anyone who asks, and running one gets your IP address blacklisted within hours and makes you a spam source. This image is not an open relay: PERMIT_DOCKER=none means only the container's own loopback is a trusted network, so anything arriving from outside must authenticate before it can relay. You can confirm this yourself from another machine:

printf 'EHLO probe\r\nMAIL FROM:<probe@example.net>\r\nRCPT TO:<victim@example.org>\r\nQUIT\r\n' | nc <vm-ip> 25

The RCPT TO must be refused with 554 5.7.1 <victim@example.org>: Relay access denied. If it is ever accepted, stop and investigate before putting the server into service.

Setting your own domain

The default mail hostname is derived from the instance's own name and is only useful for testing. Point a DNS A record at the instance (use its Elastic IP), then set the real hostname. This regenerates the TLS certificate for the new name and restarts the server:

sudo /usr/local/sbin/dms-set-domain.sh mail.<your-domain>

Then create your mailboxes. Omit the password argument to be prompted for it rather than putting it in your shell history:

sudo docker exec -ti mailserver setup email add you@<your-domain>
sudo docker exec mailserver setup email list

DNS records

For mail to work reliably you need these records on your domain:

Type Name Value Purpose
A mail your instance's Elastic IP Resolves the mail host
MX @ 10 mail.your-domain. Tells other servers where to deliver
TXT @ v=spf1 mx ~all SPF: authorises this host to send
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@your-domain DMARC policy
PTR (reverse of your IP) mail.your-domain. Reverse DNS — see below

Generate a DKIM key and publish it:

sudo docker exec mailserver setup config dkim domain <your-domain>
sudo cat /var/lib/docker-mailserver/config/rspamd/dkim/*.public.dns.txt

Publish the contents as a TXT record at the name shown in that file.

Reverse DNS matters more than people expect. Many large providers reject mail from hosts with no PTR record or a PTR that does not match the sending hostname. On AWS, reverse DNS for an Elastic IP is set through the same "Request to remove email sending limitations" form used to lift the outbound port 25 throttle — supply the Elastic IP and the desired reverse DNS name (mail.your-domain). If you send through a relay instead, the relay's reputation and DNS records are what recipients see, so reverse DNS matters most when you deliver directly.

Sending mail: the authenticated relay

Until the AWS outbound port 25 limit is removed for your account, configure an authenticated smarthost. This works immediately and is the recommended path for most deployments. Use any provider that offers authenticated SMTP on port 587 — Amazon SES, SendGrid, Mailgun, Postmark, or your organisation's own relay:

# Example with Amazon SES: <relay-host> is your SES SMTP endpoint (e.g.
# email-smtp.us-east-1.amazonaws.com) and <relay-user> is your SES SMTP
# username; use your SMTP credentials, not your AWS access keys.
sudo /usr/local/sbin/dms-set-relay.sh <relay-host> 587 <relay-user> <your-token>

The script writes DEFAULT_RELAY_HOST, RELAY_HOST, RELAY_PORT, RELAY_USER and RELAY_PASSWORD into /opt/docker-mailserver/mailserver.env (mode 0600) and restarts the server. It warns you if you pass port 25, since that will time out until the AWS limit is removed.

Confirm Postfix picked it up:

sudo docker exec mailserver postconf relayhost

Then send a real message through your relay and watch it leave:

sudo docker exec mailserver tail -f /var/log/mail/mail.log

A successful relayed delivery logs status=sent with your relay host in the relay= field. If you see status=deferred with Connection timed out to port 25, you are still trying to deliver directly and the relay is not configured.

Check whether outbound 25 is throttled. Measure it rather than guessing:

timeout 12 bash -c 'exec 3<>/dev/tcp/alt1.aspmx.l.google.com/25' && echo "port 25 REACHABLE" || echo "port 25 BLOCKED - use a relay or request removal"
timeout 12 bash -c 'exec 3<>/dev/tcp/email-smtp.us-east-1.amazonaws.com/587' && echo "port 587 REACHABLE" || echo "port 587 blocked"

Connecting a mail client

Setting Value
Incoming (IMAP) server mail.your-domain, port 993, SSL/TLS
Outgoing (SMTP) server mail.your-domain, port 587, STARTTLS
Username the full email address, e.g. you@your-domain
Password the password you set with setup email add
Authentication normal password

Until you install a real certificate your client will warn about the self signed certificate. That is expected — the connection is still encrypted, but the identity is not verified by a public authority.

Installing a real TLS certificate

Use any ACME client on the host to obtain a certificate for mail.your-domain, then point the mail server at it. With certbot in standalone mode (port 80 must be reachable and free):

sudo apt-get install -y certbot
sudo certbot certonly --standalone -d mail.your-domain
sudo cp /etc/letsencrypt/live/mail.your-domain/fullchain.pem /var/lib/docker-mailserver/config/ssl/cert.pem
sudo cp /etc/letsencrypt/live/mail.your-domain/privkey.pem /var/lib/docker-mailserver/config/ssl/key.pem
sudo systemctl restart docker-mailserver.service

The paths cert.pem and key.pem in the config volume are what SSL_TYPE=manual points at, so no configuration change is needed — replace the files and restart. Add the same copy commands to a certbot renewal hook so renewals take effect.

Spam filtering and DKIM

This image runs Rspamd rather than upstream's default Amavis and SpamAssassin combination. Rspamd handles spam scoring, DKIM signing and verification, SPF and DMARC in a single daemon, replacing five separate services. That is materially less memory for the same function, and it is upstream's own documented modern stack.

Check that it is scanning:

sudo docker exec mailserver rspamc stat
sudo docker exec mailserver tail -5 /var/log/mail/rspamd.log

Messages scanned climbing above zero is the proof that Rspamd is actually in the mail path, rather than merely running.

Spam is delivered to the recipient's Junk folder rather than rejected outright, so nothing is silently lost while you tune. Moving a message into or out of Junk trains the Bayesian classifier, because RSPAMD_LEARN=1 is set.

Brute force protection

The mail ports are internet facing, so Fail2ban is enabled with the NET_ADMIN capability the container needs to actually install bans. On an instance with a public IP you will typically see real brute force attempts within minutes of first boot.

sudo docker exec mailserver setup fail2ban status

Unban an address if you lock yourself out:

sudo docker exec mailserver setup fail2ban unban 203.0.113.10

Backing up mail

All mail state is on the dedicated data volume at /var/lib/docker-mailserver:

findmnt -no SOURCE,TARGET,FSTYPE,SIZE /var/lib/docker-mailserver
sudo du -sh /var/lib/docker-mailserver/mail-data
Path Contents
mail-data/ The Maildir tree — every message
config/ Accounts, aliases, DKIM keys, TLS material
mail-state/ Runtime state, Rspamd's Bayes database
mail-logs/ Mail logs

Because it is a separate EBS volume you can snapshot it independently of the OS disk, and resize it without rebuilding the instance. Snapshot it on a schedule; config/ in particular holds your DKIM private keys, which cannot be regenerated without republishing DNS.

Licences and source code

docker-mailserver itself is MIT licensed. The container bundles third party components under a range of licences, including several under the GNU GPL v2. The upstream container image is built on debian:12-slim, which strips /usr/share/doc, so cloudimg recovers those copyright notices at build time and ships them in this image:

ls /opt/docker-mailserver/licenses/copyright/
cat /opt/docker-mailserver/licenses/copyright/fail2ban/copyright
head -20 /opt/docker-mailserver/licenses/common-licenses/GPL-2
wc -l /opt/docker-mailserver/licenses/PACKAGE-MANIFEST.txt
Component Licence
docker-mailserver MIT
Postfix EPL-2.0 or IPL-1.0 (recipient's choice)
Dovecot LGPL-2.1 core, MIT in parts
Rspamd Apache-2.0
OpenDKIM / OpenDMARC BSD-3-Clause, Sendmail Open Source Licence
Fail2ban GPL-2.0-or-later
ClamAV GPL-2.0-only
Amavis GPL-2.0-or-later

The GPL v2 components carry a source availability obligation. GPL v2 section 3(c), which lets a distributor pass along the offer it received, applies only to non commercial distribution, and GPL v2 has no equivalent of GPL v3's "point at a third party server" option — so cloudimg makes its own written offer under section 3(b). It is shipped in the image:

cat /opt/docker-mailserver/THIRD-PARTY-LICENSES.md

The exact package versions the offer is pinned to are in /opt/docker-mailserver/licenses/PACKAGE-MANIFEST.txt. To request source, contact support@cloudimg.co.uk quoting the image digest and the component you need.

Troubleshooting

The container keeps restarting and the logs say "You need at least one mail account to start Dovecot". docker-mailserver requires at least one mailbox. First boot creates the postmaster account automatically; if you deleted every account, add one back with setup email add.

Mail sits in the queue with Connection timed out to port 25. This is the AWS outbound port 25 throttle. Configure a relay with dms-set-relay.sh as described above, or request removal of the limit. Inspect the queue with:

sudo docker exec mailserver postqueue -p

Mail is delivered to recipients' spam folders. Check SPF, DKIM, DMARC and reverse DNS. Send a message to a mail tester service and review the report. If you relay, most of this is governed by the relay provider's configuration.

A mail client cannot authenticate. The username is the full email address, not the local part. Confirm the account exists with setup email list and check sudo docker exec mailserver tail -50 /var/log/mail/mail.log for the rejection reason.

Locked out by Fail2ban. Unban your address as shown above, from an SSH session.

Take a quick health snapshot:

systemctl is-active docker.service docker-mailserver.service
sudo docker ps --format '{{.Names}}  {{.Status}}'
sudo docker exec mailserver setup email list

For the full end to end proof (submission, delivery, IMAP retrieval and the open relay check) re-run sudo /usr/local/sbin/dms-selftest.sh from the verification section above.

Optional: enabling ClamAV antivirus

ClamAV is present in the image but disabled by default, which is also upstream's default. Upstream's own FAQ notes that ClamAV reads its entire signature database into RAM — around 850 MB and growing — and that without swap you may need 3 GB for it alone. Enabling it by default would put a small instance into out of memory failure on first boot, so it is opt in:

# Run this only on an instance with at least 8 GB RAM (m5.large or larger).
# The script refuses to run below 6 GB rather than let ClamAV exhaust memory.
sudo /usr/local/sbin/dms-enable-clamav.sh

The script refuses to run on an instance with less than 6 GB of RAM and tells you to resize first. That guard is deliberate and was set from measurement, not guesswork: on a 4 GB instance ClamAV's signature load pushes the machine into swap and stalls the mail server. On the recommended m5.large (8 GB) there is ample headroom. The first signature refresh takes several minutes; watch it with sudo docker exec mailserver tail -f /var/log/mail/mail.log.

Support

cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk.