docker-mailserver
Overview
docker-mailserver (DMS) is a production-ready, full-stack mail server. It combines 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. You configure it all through a single environment file and a plain file-based account store. There is no database to run and no web console to secure.
This guide covers deploying docker-mailserver on Ubuntu 24.04 on Azure from SecureAnyCloud Azure Marketplace images. The 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.
What you get:
- Mail storage on a dedicated, independently resizable Azure data disk.
- No known credential. The image contains no mailbox at all. A unique postmaster password and a unique self-signed TLS certificate are generated on each VM at first boot, before the server is reachable.
- 24/7 SecureAnyCloud support.
docker-mailserver is a trademark of its respective owner. SecureAnyCloud produces this image. It 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.
Before you start: Azure blocks outbound port 25
Read this before you deploy a mail server on Azure. It is the most important constraint.
Microsoft blocks outbound TCP port 25 from Azure virtual machines. Microsoft's current guidance is:
- Enterprise Agreement (EA) and Microsoft Customer Agreement for enterprise (MCA-E) subscriptions are not blocked.
- Enterprise Dev/Test subscriptions are blocked by default but can be exempted. Use the virtual network's Diagnose and solve problems > Cannot send email (SMTP-Port 25) self-service flow. You must then stop, deallocate and restart the VMs.
- All other subscription types are blocked, and no exemption is available. These are pay as you go, MSDN, CSP, Free Trial, Azure Pass, Azure in Open and Education. Requests to lift the block are not granted.
What this means for this image on any Azure subscription:
- Receiving mail on port 25 from other mail servers: works. Inbound is completely unaffected.
- IMAP mailbox access on 143 and 993: works.
- Authenticated submission from your mail clients on 587 and 465: works.
- Sending by connecting directly to recipient MX servers on port 25: works only on EA and MCA-E.
- Sending through an authenticated smarthost relay on port 587: works. This is Microsoft's own recommendation.
As shipped, this is a fully functional receiving and IMAP mail server on any subscription. To send mail, you configure a relay. This is not a limitation of docker-mailserver or of this image. It is an Azure platform policy that applies to every VM-based mail server.
The image is configured relay-first for this reason. RELAY_PORT defaults to 587 rather than upstream's default of 25, which would silently time out.
The screenshot below shows this measurement taken on a real Azure VM. It does not come from documentation.
What you get:
- 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:
25SMTP,143IMAP4 STARTTLS,465ESMTP implicit TLS,587ESMTP STARTTLS and993IMAP4 implicit TLS. - Fail2ban enabled with the
NET_ADMINcapability so bans are actually enforced, guarding the internet-facing ports. - A dedicated 64 GiB ext4 Azure data disk at
/var/lib/docker-mailserverthat holds the Maildir, mail state, mail logs and configuration, and is independently resizable. - A unique self-signed TLS certificate and a unique postmaster mailbox password, generated per VM on first boot and never baked into the image.
- No mailbox and no credential in the shipped image at all.
- ClamAV present but disabled by default. Enable it with a single command when you have the RAM for it.
- Bundled component license notices and a GPL v2 written offer for source, shipped in the image.
docker-mailserver.serviceanddocker-mailserver-firstboot.serviceas systemd units, enabled and active on boot.- An Ubuntu 24.04 LTS base with the latest security patches applied at build time.
- The Azure Linux Agent for cloud integration and SSH key injection.
Prerequisites
- An active Azure subscription, an SSH public key, and a VNet and subnet in the target region.
- A subscription to the docker-mailserver listing on Azure Marketplace.
- A domain name you control, with access to its DNS records.
- An authenticated SMTP relay account if you need to send mail (see the sending section).
Recommended virtual machine size: Standard_B2ms (2 vCPU, 8 GB RAM). The shipped stack (Postfix, Dovecot, Rspamd with its Redis, Fail2ban) runs in roughly 1 to 1.5 GB, so Standard_B2s (2 vCPU, 4 GB) is workable for evaluation. 8 GB is recommended because it leaves headroom for mailbox volume and for enabling ClamAV, which upstream documents as needing around 3 GB on its own. For high mail volume, use Standard_D2s_v3 or larger.
NSG inbound rules. Allow 22/tcp from your management network only. Open the mail ports to the networks that use them:
- 25/tcp: SMTP for inbound mail from other mail servers. Open it to the internet if you receive mail from outside.
- 587/tcp: submission with STARTTLS. Open it to your users' mail clients.
- 465/tcp: submission with implicit TLS. Open it to your users' mail clients.
- 143/tcp: IMAP4 with STARTTLS. Open it to your users' mail clients.
- 993/tcp: IMAP4 with implicit TLS. Open it to your users' mail clients.
Port 25 is the only port that needs to be open to the whole internet, and only if you want to receive mail from arbitrary senders. If this server only sends through a relay and serves mail over IMAP, keep 25 closed to the internet.
Deploying from the Azure Marketplace
az vm image terms accept --urn sac1647283583153:docker-mailserver-ubuntu-24-04:default:latest
az vm create \
--resource-group my-mail-rg \
--name my-mailserver \
--image sac1647283583153:docker-mailserver-ubuntu-24-04:default:latest \
--size Standard_B2ms \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard \
--location eastusThen open the mail ports on the network security group.
az network nsg rule create -g my-mail-rg --nsg-name my-mailserverNSG --name allow-smtp \
--priority 1001 --destination-port-ranges 25 --access Allow --protocol Tcp
az network nsg rule create -g my-mail-rg --nsg-name my-mailserverNSG --name allow-submission \
--priority 1002 --destination-port-ranges 587 465 --access Allow --protocol Tcp \
--source-address-prefixes "<your-mgmt-cidr>"
az network nsg rule create -g my-mail-rg --nsg-name my-mailserverNSG --name allow-imap \
--priority 1003 --destination-port-ranges 993 143 --access Allow --protocol Tcp \
--source-address-prefixes "<your-mgmt-cidr>"First boot
On the first boot of each VM, docker-mailserver-firstboot.service derives this VM'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.serviceThe output looks like this.
active activeConfirm that the container is running and pinned by digest.
sudo docker ps --format '{{.Names}} {{.Image}} {{.Status}}' cat /opt/docker-mailserver/IMAGE-DIGEST.txtThe digest recorded there is the exact image this VM runs.
ghcr.io/docker-mailserver/docker-mailserver@sha256:af51b15dd3fc72153c0e90eb7692bb5e3a463212d87959a80fa7aa89b617d44aConfirm that 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 -uPer-VM credentials
Everything unique to this VM is written to /root/docker-mailserver-credentials.txt, mode 0600, owned by root.
sudo cat /root/docker-mailserver-credentials.txtAbout the one mailbox. docker-mailserver will not start Dovecot with zero accounts. It warns and shuts the container down after 120 seconds.
First boot therefore creates exactly one mailbox: the postmaster@ address, which RFC 5321 requires every mail server to accept anyway. The password is generated randomly on your VM at first boot. It is not present in the image or in any configuration file, and SecureAnyCloud does not know it.
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 only checking that a port is open. It creates nothing and leaves nothing behind.
sudo /usr/local/sbin/dms-selftest.shThe self-test submits a message on port 587 with STARTTLS and SMTP authentication. It polls IMAP on 993 until the message is delivered, fetches it, asserts the exact content, then removes it.
It also runs three negative controls, and each must 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 open relay check is the important one. An open relay forwards mail to arbitrary external domains for anyone who asks. 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 that arrives from outside must authenticate before it can relay. You can confirm this 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> 25The RCPT TO must be refused with 554 5.7.1 <victim@example.org>: Relay access denied. If the server ever accepts it, stop and investigate before you put the server into service.
Setting your own domain
The default mail hostname is derived from the VM's own name and is only useful for testing. Point a DNS A record at the VM, 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 so the command prompts for it, which keeps the password out of your shell history.
sudo docker exec -ti mailserver setup email add you@<your-domain>
sudo docker exec mailserver setup email listDNS records
For mail to work reliably, create these records on <your-domain>.
- A record, name mail, value your VM's public IP. It resolves the mail host.
- MX record, name @, value 10 mail.<your-domain>. It tells other servers where to deliver.
- TXT record, name @, value v=spf1 mx ~all. This is SPF, and it authorizes this host to send.
- TXT record, name _dmarc, value v=DMARC1; p=quarantine; rua=mailto:postmaster@<your-domain>. This is your DMARC policy.
- PTR record, name (reverse of your IP), value mail.<your-domain>. This is reverse DNS, and you set it on the Azure public IP resource.
Generate a DKIM key and publish it.
sudo docker exec mailserver setup config dkim
sudo cat /var/lib/docker-mailserver/config/rspamd/dkim/*.public.dns.txtPublish 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 that have no PTR record, or a PTR that does not match the sending hostname. Set it on the Azure public IP resource.
az network public-ip update --resource-group my-mail-rg --name my-mailserverPublicIP --reverse-fqdn mail.<your-domain>If you send through a relay, which is the normal case on Azure, recipients see the relay's reputation and DNS records. Reverse DNS therefore matters most on EA and MCA-E subscriptions that deliver directly.
Sending mail: the authenticated relay
Because Azure blocks outbound port 25 on most subscriptions, configure an authenticated smarthost. This is Microsoft's own recommendation and works on every subscription type.
Use any provider that offers authenticated SMTP on port 587, such as Azure Communication Services, SendGrid, Mailgun, Amazon SES, Postmark, or your organization's own relay.
# Example with SendGrid: the user is literally "apikey" and the password is the API key.
sudo /usr/local/sbin/dms-set-relay.sh smtp.sendgrid.net 587 apikey <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, because that times out on most Azure subscriptions.
Confirm that Postfix picked it up.
sudo docker exec mailserver postconf relayhostThen send a real message through your relay and watch it leave.
sudo docker exec mailserver tail -f /var/log/mail/mail.logA 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, the server is still trying to deliver directly and the relay is not configured.
Check whether your subscription is blocked. Measure it rather than guess.
timeout 12 bash -c 'exec 3<>/dev/tcp/alt1.aspmx.l.google.com/25' && echo "port 25 REACHABLE (EA/MCA-E)" || echo "port 25 BLOCKED - use a relay" timeout 12 bash -c 'exec 3<>/dev/tcp/smtp.sendgrid.net/587' && echo "port 587 REACHABLE" || echo "port 587 blocked"Connecting a mail client
Enter these settings in your mail client.
- Incoming (IMAP) server: mail.<your-domain>, port 993, SSL/TLS
- Outgoing (SMTP) server: mail.<your-domain>, port 587, STARTTLS
- Username: the full email address, for example you@<your-domain>
- Password: the password you set with setup email add
- Authentication: normal password
Until you install a real certificate, your client warns about the self-signed certificate. This is expected. The connection is still encrypted, but no public authority verifies the server's identity.
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. This example uses 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.serviceThe 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, and replaces five separate services. It uses 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.logWhen Messages scanned climbs above zero, Rspamd is in the mail path and not merely running.
Spam goes to the recipient's Junk folder rather than being 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 install bans. On a VM with a public IP you will typically see real brute force attempts within minutes of first boot.
sudo docker exec mailserver setup fail2ban statusIf you lock yourself out, unban your address.
sudo docker exec mailserver setup fail2ban unban 203.0.113.10Backing up mail
All mail state is on the dedicated data disk at /var/lib/docker-mailserver:
findmnt -no SOURCE,TARGET,FSTYPE,SIZE /var/lib/docker-mailserver
sudo du -sh /var/lib/docker-mailserver/mail-data- mail-data/: the Maildir tree, which holds every message.
- config/: accounts, aliases, DKIM keys and TLS material.
- mail-state/: runtime state and Rspamd's Bayes database.
- mail-logs/: mail logs.
Because it is a separate Azure managed disk, you can snapshot it independently of the OS disk and resize it without rebuilding the VM. Snapshot it on a schedule. config/ in particular holds your DKIM private keys, which you cannot regenerate without republishing DNS.
Licenses and source code
docker-mailserver itself is MIT licensed. The container bundles third-party components under a range of licenses, including several under the GNU GPL v2. The upstream container image is built on debian:12-slim, which strips /usr/share/doc. SecureAnyCloud therefore 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- 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 License
- 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) lets a distributor pass along the offer it received, but it applies only to non-commercial distribution. GPL v2 also has no equivalent of GPL v3's "point at a third party server" option. SecureAnyCloud therefore makes its own written offer under section 3(b). The offer ships in the image.
cat /opt/docker-mailserver/THIRD-PARTY-LICENSES.mdThe exact package versions the offer is pinned to are in /opt/docker-mailserver/licenses/PACKAGE-MANIFEST.txt. To request source, contact sales@secureanycloud.com and quote 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 Azure outbound block. Configure a relay with dms-set-relay.sh as described above. Inspect the queue with this command.
sudo docker exec mailserver postqueue -pMail lands in 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, the relay provider's configuration governs most of this.
A mail client cannot authenticate. The username is the full email address, not the local part. Confirm the account exists with setup email list. Check sudo docker exec mailserver tail -50 /var/log/mail/mail.log for the rejection reason.
Fail2ban locked you out. From an SSH session, unban your address as shown above.
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 listFor 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 VM into out-of-memory failure on first boot, so it is opt-in.
# Run this only after resizing the VM to Standard_B2ms (8 GB) or larger.
# The script refuses to run below 6 GB rather than let ClamAV exhaust memory.
sudo /usr/local/sbin/dms-enable-clamav.shThe script refuses to run on a VM with less than 6 GB of RAM and tells you to resize first. The guard is deliberate and based on measurement: on a 4 GB VM, this build watched ClamAV's signature load push the machine into swap and stall the mail server.
On the recommended Standard_B2ms (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
SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com.
Deploy on Azure
Launch docker-mailserver on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
docker-mailserver (DMS) is a production-ready, full-stack mail server. It combines 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. You configure all of it through a single environment file and a plain file-based account store. There is no database to run and no web console to secure.
This guide covers deployment and configuration on AWS with SecureAnyCloud's preconfigured Amazon Machine Image. The 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. Backed by 24/7 SecureAnyCloud support.
What you get:
- Mail storage on a dedicated, independently resizable EBS data volume.
- No known credential in the image. The image contains no mailbox at all.
- A unique postmaster password and a unique self-signed TLS certificate, generated on each instance at first boot, before the server is reachable.
docker-mailserver is a trademark of its respective owner. This image is produced by SecureAnyCloud 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.
Before you start: AWS throttles outbound port 25
Read this before you deploy a mail server on AWS. It is the most important constraint.
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 do both of the following.
- Complete the AWS "Request to remove email sending limitations" form for your account and region.
- Set up reverse DNS (a PTR record) for the sending IP. AWS provisions reverse DNS for Elastic IP addresses through the same request.
Until AWS grants that request, direct-to-MX delivery on port 25 times out. In the meantime, relay outbound mail through an authenticated smarthost on port 587, which AWS does not throttle. This is the supported and recommended path, and the simplest one for most deployments.
What this means in practice for this image:
- Receiving mail on port 25 from other mail servers: works. Inbound is completely unaffected.
- IMAP mailbox access on 143 and 993: works.
- Authenticated submission from your mail clients on 587 and 465: works.
- Sending by connecting directly to recipient MX servers on port 25: works only after AWS removes the outbound 25 limit and reverse DNS is set.
- Sending through an authenticated smarthost relay on port 587: works. This is the recommended path.
As shipped, the image is a fully functional receiving and IMAP mail server. For sending, 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. RELAY_PORT defaults to 587 rather than upstream's default of 25, which would silently time out.
The screenshot below shows this measurement taken on a real EC2 instance.
What you get:
- 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:
25SMTP,143IMAP4 STARTTLS,465ESMTP implicit TLS,587ESMTP STARTTLS,993IMAP4 implicit TLS. - Fail2ban enabled with the
NET_ADMINcapability, so bans are enforced on the internet-facing ports. - A dedicated 64 GiB ext4 EBS data volume at
/var/lib/docker-mailserver. It holds the Maildir, mail state, mail logs and configuration, and you can resize it independently. - A unique self-signed TLS certificate and a unique postmaster mailbox password, generated per instance on first boot and never baked into the image.
- No mailbox and no credential in the shipped image at all.
- ClamAV present but disabled by default. You enable it with a single command when you have the RAM for it.
- Bundled component license notices and a GPL v2 written offer for source, shipped in the image.
docker-mailserver.serviceanddocker-mailserver-firstboot.serviceas systemd units, enabled and active on boot.- Ubuntu 24.04 LTS base with the 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 AWS removes the port 25 limit (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 leaves 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. Make the mail ports reachable from the networks that use them.
- 25/tcp: SMTP for inbound mail from other mail servers. Open it to the internet if you receive mail from outside.
- 587/tcp: submission with STARTTLS. Open it to your users' mail clients.
- 465/tcp: submission with implicit TLS. Open it to your users' mail clients.
- 143/tcp: IMAP4 with STARTTLS. Open it to your users' mail clients.
- 993/tcp: IMAP4 with implicit TLS. Open it to your users' mail clients.
Port 25 is the only port that needs to be open to the whole internet, and only if you want to receive mail from arbitrary senders. If this server only sends through a relay and serves mail over IMAP, keep 25 closed to the internet.
Connecting to your instance
SSH in as the default login user for the operating system variant you launched.
- Ubuntu 24.04: SSH login user ubuntu. Example: ssh -i your-key.pem ubuntu@<instance-public-ip>
There is no web console. You administer docker-mailserver over SSH and with standard mail clients. Run everything below from an SSH session on the instance.
Deploying from the AWS Marketplace
Subscribe to the listing in AWS Marketplace, then 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, with 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.serviceThe output looks like this.
active activeConfirm that the container is running and pinned by digest.
sudo docker ps --format '{{.Names}} {{.Image}} {{.Status}}' cat /opt/docker-mailserver/IMAGE-DIGEST.txtThe digest recorded there is the exact image this instance runs.
ghcr.io/docker-mailserver/docker-mailserver@sha256:af51b15dd3fc72153c0e90eb7692bb5e3a463212d87959a80fa7aa89b617d44aConfirm that 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 -uPer-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.txtAbout the one mailbox. docker-mailserver will not start Dovecot with zero accounts. It warns and shuts the container down after 120 seconds.
First boot therefore creates exactly one mailbox: the postmaster@ address, which RFC 5321 requires every mail server to accept anyway. The password is generated randomly on your instance at first boot. It is not present in the image or in any configuration file, and SecureAnyCloud does not know it.
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 only checking that a port is open. It creates nothing and leaves nothing behind.
sudo /usr/local/sbin/dms-selftest.shThe self-test submits a message on port 587 with STARTTLS and SMTP authentication. It polls IMAP on 993 until the message is delivered, fetches it, asserts the exact content, then removes it.
It also runs three negative controls, and each must 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 open relay check is the important one. An open relay forwards mail to arbitrary external domains for anyone who asks. 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 that arrives from outside must authenticate before it can relay. You can confirm this 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> 25The RCPT TO must be refused with 554 5.7.1 <victim@example.org>: Relay access denied. If the server ever accepts it, stop and investigate before you put 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, using 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 so the command prompts for it, which keeps the password out of your shell history.
sudo docker exec -ti mailserver setup email add you@<your-domain>
sudo docker exec mailserver setup email listDNS records
For mail to work reliably, create these records on your domain.
- A record, name mail, value your instance's Elastic IP. It resolves the mail host.
- MX record, name @, value 10 mail.your-domain. It tells other servers where to deliver.
- TXT record, name @, value v=spf1 mx ~all. This is SPF, and it authorizes this host to send.
- TXT record, name _dmarc, value v=DMARC1; p=quarantine; rua=mailto:postmaster@your-domain. This is your DMARC policy.
- PTR record, name (reverse of your IP), value mail.your-domain. This is 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.txtPublish 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 that have no PTR record, or a PTR that does not match the sending hostname.
On AWS, you set reverse DNS for an Elastic IP through the same "Request to remove email sending limitations" form that lifts the outbound port 25 throttle. Supply the Elastic IP and the reverse DNS name you want (mail.your-domain).
If you send through a relay instead, recipients see the relay's reputation and DNS records. Reverse DNS therefore 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, such as Amazon SES, SendGrid, Mailgun, Postmark, or your organization'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, because that times out until AWS removes the limit.
Confirm that Postfix picked it up.
sudo docker exec mailserver postconf relayhostThen send a real message through your relay and watch it leave.
sudo docker exec mailserver tail -f /var/log/mail/mail.logA 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, the server is still trying to deliver directly and the relay is not configured.
Check whether outbound 25 is throttled. Measure it rather than guess.
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
Enter these settings in your mail client.
- Incoming (IMAP) server: mail.your-domain, port 993, SSL/TLS
- Outgoing (SMTP) server: mail.your-domain, port 587, STARTTLS
- Username: the full email address, for example you@your-domain
- Password: the password you set with setup email add
- Authentication: normal password
Until you install a real certificate, your client warns about the self-signed certificate. This is expected. The connection is still encrypted, but no public authority verifies the server's identity.
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. This example uses 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.serviceThe 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, and replaces five separate services. It uses 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.logWhen Messages scanned climbs above zero, Rspamd is in the mail path and not merely running.
Spam goes to the recipient's Junk folder rather than being 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 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 statusIf you lock yourself out, unban your address.
sudo docker exec mailserver setup fail2ban unban 203.0.113.10Backing 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- mail-data/: the Maildir tree, which holds every message.
- config/: accounts, aliases, DKIM keys and TLS material.
- mail-state/: runtime state and 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 you cannot regenerate without republishing DNS.
Licenses and source code
docker-mailserver itself is MIT licensed. The container bundles third-party components under a range of licenses, including several under the GNU GPL v2. The upstream container image is built on debian:12-slim, which strips /usr/share/doc. SecureAnyCloud therefore 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- 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 License
- 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) lets a distributor pass along the offer it received, but it applies only to non-commercial distribution. GPL v2 also has no equivalent of GPL v3's "point at a third party server" option. SecureAnyCloud therefore makes its own written offer under section 3(b). The offer ships in the image.
cat /opt/docker-mailserver/THIRD-PARTY-LICENSES.mdThe exact package versions the offer is pinned to are in /opt/docker-mailserver/licenses/PACKAGE-MANIFEST.txt. To request source, contact sales@secureanycloud.com and quote 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 this command.
sudo docker exec mailserver postqueue -pMail lands in 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, the relay provider's configuration governs most of this.
A mail client cannot authenticate. The username is the full email address, not the local part. Confirm the account exists with setup email list. Check sudo docker exec mailserver tail -50 /var/log/mail/mail.log for the rejection reason.
Fail2ban locked you out. From an SSH session, unban your address as shown above.
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 listFor 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.shThe script refuses to run on an instance with less than 6 GB of RAM and tells you to resize first. The guard is deliberate and based on measurement: 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
SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com.
Deploy on AWS
Launch docker-mailserver on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com