Maddy is a mail server written in Go that replaces the usual Postfix, Dovecot and OpenDKIM stack with a single binary. It handles inbound SMTP, authenticated submission, IMAP storage, DKIM signing and SPF/DKIM/DMARC checks from one configuration file. In this tutorial you will install Maddy on Ubuntu 24.04, give it a Let's Encrypt certificate, publish the DNS records it needs and create your first mailbox, then test it with a real mail client.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS (x86_64) with at least 1 GB of RAM, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • A domain, referred to as your_domain, and a mail hostname such as mx1.your_domain with an A record pointing to the server's public IP (your_server_ip).
  • A PTR (reverse DNS) record for your_server_ip set to mx1.your_domain. Your hosting provider sets this, not your DNS zone.
  • Inbound and outbound TCP port 25 allowed by your provider. Many providers block outbound port 25 on new accounts until you ask.
  • No other mail server (Postfix, Exim) listening on ports 25, 465, 587, 143 or 993.

Step 1 - Installing the Maddy binary

Maddy is not packaged for Ubuntu, but the project publishes a static binary for Linux together with its systemd unit files. Install the tools needed to download and unpack it:

sudo apt update
sudo apt install curl zstd acl

Look up the latest release on the GitHub releases page and store the version number in a variable. At the time of writing it is 0.9.5:

MADDY_VERSION=0.9.5

Download and extract the archive:

cd /tmp
curl -fLO "https://github.com/foxcpp/maddy/releases/download/v${MADDY_VERSION}/maddy-${MADDY_VERSION}-x86_64-linux-musl.tar.zst"
tar --zstd -xf "maddy-${MADDY_VERSION}-x86_64-linux-musl.tar.zst"
cd "maddy-${MADDY_VERSION}-x86_64-linux-musl"

The directory contains the maddy binary, a default maddy.conf and a systemd folder. Install all three in their standard locations:

sudo install -m 755 maddy /usr/local/bin/maddy
sudo install -d -m 755 /etc/maddy
sudo install -m 644 maddy.conf /etc/maddy/maddy.conf
sudo install -m 644 systemd/maddy.service systemd/[email protected] /etc/systemd/system/
sudo install -d -m 755 /usr/lib/maddy
sudo systemctl daemon-reload

The empty /usr/lib/maddy directory is referenced by the sandboxing options in maddy.service; creating it avoids a namespace error at start-up.

Maddy never runs as root. Create a system user whose home is the state directory, where it keeps its databases, queue and DKIM keys:

sudo useradd -mrU -s /usr/sbin/nologin -d /var/lib/maddy -c "maddy mail server" maddy

Confirm that the binary runs:

maddy version

The command prints the installed version, 0.9.5 in this example, followed by the platform and Go version it was built with.

Step 2 - Setting the hostname and domain

Maddy's configuration is built from small modules (endpoints, storage, checks, modifiers) that you wire together. The default file already contains a complete setup for a single domain, so you only need to set three variables. Open it:

sudo nano /etc/maddy/maddy.conf

Change the base variables at the top of the file:

$(hostname) = mx1.your_domain
$(primary_domain) = your_domain
$(local_domains) = $(primary_domain)

$(hostname) is the name Maddy uses in its SMTP greeting and must match your PTR record. $(local_domains) is the list of domains Maddy accepts mail for; to host more domains later, append them separated by spaces.

Leave the tls file line below the variables as it is. It points to /etc/maddy/certs/$(hostname)/, which you will link to Let's Encrypt in the next step. Save and close the file.

The default configuration already listens on these ports:

PortEndpointPurpose
25smtpMail from other servers
465submission (implicit TLS)Sending from mail clients
587submission (STARTTLS)Sending from mail clients
993imap (implicit TLS)Reading mail
143imap (STARTTLS)Reading mail

Step 3 - Getting a TLS certificate

Maddy cannot start without a certificate. Use Certbot in standalone mode, which briefly listens on port 80 to answer the Let's Encrypt challenge.

Open the ports Maddy and Certbot need in UFW:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 25,465,587,143,993/tcp
sudo ufw enable

Install Certbot and request the certificate, replacing admin@your_domain with an address that receives expiry notices:

sudo apt install certbot
sudo certbot certonly --standalone -d mx1.your_domain -m admin@your_domain --agree-tos --no-eff-email
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/mx1.your_domain/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/mx1.your_domain/privkey.pem

Point Maddy's certificate directory at the Let's Encrypt one and give the maddy user read access to the keys with an ACL, so they stay protected from other users:

sudo ln -s /etc/letsencrypt/live /etc/maddy/certs
sudo setfacl -R -m u:maddy:rX /etc/letsencrypt/live /etc/letsencrypt/archive

Certbot writes new key files on every renewal, so the ACL has to be applied again each time. Create a deploy hook, which Certbot runs after each successful renewal:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/maddy.sh
#!/usr/bin/env bash
set -euo pipefail
setfacl -R -m u:maddy:rX /etc/letsencrypt/live /etc/letsencrypt/archive
systemctl reload maddy
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/maddy.sh

Maddy also rereads certificates from disk about once a minute, so the reload only makes the switch immediate. Check that the maddy user can read the key:

sudo -u maddy head -c 27 /etc/maddy/certs/mx1.your_domain/privkey.pem; echo
-----BEGIN PRIVATE KEY-----

Step 4 - Starting Maddy

Enable and start the service:

sudo systemctl enable --now maddy

Check that it is running:

sudo systemctl status maddy
● maddy.service - maddy mail server
     Loaded: loaded (/etc/systemd/system/maddy.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-25 10:20:41 UTC; 5s ago

Confirm it is listening on all five ports:

sudo ss -tlnp | grep maddy
LISTEN 0  4096  *:25   *:*  users:(("maddy",pid=2310,fd=9))
LISTEN 0  4096  *:465  *:*  users:(("maddy",pid=2310,fd=10))
LISTEN 0  4096  *:587  *:*  users:(("maddy",pid=2310,fd=11))
LISTEN 0  4096  *:993  *:*  users:(("maddy",pid=2310,fd=12))
LISTEN 0  4096  *:143  *:*  users:(("maddy",pid=2310,fd=13))

If the service fails, read the log with sudo journalctl -u maddy -e. A missing or unreadable certificate is the most common cause.

Step 5 - Publishing DNS records

On its first start Maddy generated a 2048-bit RSA DKIM key for your domain with the selector default. Print the DNS record it prepared:

sudo cat /var/lib/maddy/dkim_keys/your_domain_default.dns
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx4...

Create the following records in your DNS provider. Replace the DKIM value with the output above:

NameTypeValue
your_domainMX10 mx1.your_domain
mx1.your_domainAyour_server_ip
your_domainTXTv=spf1 mx ~all
mx1.your_domainTXTv=spf1 a ~all
_dmarc.your_domainTXTv=DMARC1; p=quarantine; rua=mailto:postmaster@your_domain
default._domainkey.your_domainTXTv=DKIM1; k=rsa; p=MIIBIjAN...

Once they propagate, verify each one:

dig +short MX your_domain
dig +short TXT your_domain
dig +short TXT default._domainkey.your_domain
dig +short -x your_server_ip
10 mx1.your_domain.
"v=spf1 mx ~all"
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx4..."
mx1.your_domain.

Step 6 - Creating a mailbox

Maddy keeps login credentials and IMAP mailboxes separately, so each user needs both. Usernames are full email addresses. Run the commands as the maddy user so the database files keep the right owner.

Create the credentials. Maddy prompts for the password:

sudo -u maddy maddy creds create postmaster@your_domain

Then create the IMAP account that stores the mail:

sudo -u maddy maddy imap-acct create postmaster@your_domain

List both to confirm:

sudo -u maddy maddy creds list
sudo -u maddy maddy imap-acct list
postmaster@your_domain
postmaster@your_domain

Other useful subcommands are maddy creds password to change a password and maddy creds remove together with maddy imap-acct remove to delete a user. Run maddy creds --help for the full list.

Step 7 - Testing sending and receiving

Check the TLS certificate that IMAP clients will see:

openssl s_client -connect mx1.your_domain:993 -servername mx1.your_domain -brief </dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_128_GCM_SHA256
Peer certificate: CN = mx1.your_domain
Verification: OK

Send an authenticated test message through the submission port with swaks, a command-line SMTP client:

sudo apt install swaks
swaks --server mx1.your_domain --port 587 --tls \
  --auth-user postmaster@your_domain \
  --from postmaster@your_domain \
  --to [email protected]

swaks prompts for the password and prints the SMTP dialogue. It should end with:

<-  250 2.0.0 OK: queued
 -> QUIT
<-  221 2.0.0 Bye

In Gmail, open the message and choose Show original: SPF, DKIM and DMARC must all show PASS. Reply to it to test inbound delivery, then configure a mail client such as Thunderbird with these settings:

  • IMAP: mx1.your_domain, port 993, SSL/TLS, normal password.
  • SMTP: mx1.your_domain, port 465 (SSL/TLS) or 587 (STARTTLS), normal password.
  • Username: the full address, postmaster@your_domain.

Step 8 - Adding aliases (optional)

The default configuration reads aliases from /etc/maddy/aliases, one mapping per line. Create the file:

sudo nano /etc/maddy/aliases
abuse@your_domain: postmaster@your_domain
info@your_domain: postmaster@your_domain

Reload Maddy to apply it:

sudo systemctl reload maddy

Plus addressing also works out of the box: mail for postmaster+shop@your_domain lands in the postmaster@your_domain mailbox.

Troubleshooting

systemctl status maddy shows "address already in use". Another MTA is running. Find it with sudo ss -tlnp | grep ':25 ' and stop and disable it, for example sudo systemctl disable --now postfix.

The log shows "permission denied" for privkey.pem. The ACLs were not applied to the archive directory, where the real files live. Repeat the setfacl command from Step 3 and check that the deploy hook is executable.

Outgoing mail stays queued with "connection timed out" on port 25. Your provider blocks outbound SMTP. Test with nc -vz gmail-smtp-in.l.google.com 25 and ask the provider to unblock the port.

Mail clients reject the login. Use the full email address as the username and make sure you created both the credentials and the IMAP account.

Conclusion

You now have a complete mail server running from a single binary, with TLS, DKIM signing, SPF and DMARC checks and a working mailbox. From here you can add more domains to $(local_domains) (each gets its own DKIM key on restart), publish an MTA-STS policy to protect inbound TLS, and back up /var/lib/maddy regularly, since it holds every mailbox and the DKIM keys.