Introduction

Self-hosting an email server is useful for automating tasks like mailing lists, newsletters, or email verification APIs.

The main selling point of self-hosting is that it’s practically free. If you’re already self-hosting a website, installing some extra packages on your server and just a bit of your time is all that’s required. Mail takes very little storage and the software is light, so you’re unlikely to significantly change energy consumption or disk usage.

For the longest time, I perceived self-hosting email as too difficult, but after doing it for one of my projects, I can say it’s not much harder or more time-consuming than configuring some email SaaS.

I changed my goals a bit to make the setup easier though. Self-hosting a multi-user webmail looks heavy and is more involved than I was willing to get into, so I just skipped it. That way, I didn’t have to bother with user accounts, databases, or the web at all, and the task became easy.

With my config, manually sending and receiving email is possible if you SSH to your mail server and use the minimal sendmail or mailx commands, or Mutt if you like TUI. The setup is enough for me now, but I could expand it in the future, and multi-user webmail isn’t off the table anyway. Maybe I’ll even write a simple webmail package myself!

Setting up Postfix

You just need to open port 25, and install and configure Postfix and OpenDKIM on your machine. Postfix is a complete SMTP server, and is enough for basic mail alone, but in practice you also need OpenDKIM to get your mail delivered to popular services like Gmail.

Here's my Postfix config to show how easy it is. I left the master.cf file as it was, because I’m always submitting email locally. Notice there's no mention of POP3 or IMAP. I didn't set them up, because I didn't need them.

The default alias and header check config files are practically self-explanatory (just open them and read the comments!).

/etc/postfix/main.cf
compatibility_level = 3.8
mail_owner = postfix

myhostname = mx.idx.cy
myorigin = idx.cy
mydestination = localhost, idx.cy, maxadamski.com, localchat.cc
inet_interfaces = all
inet_protocols = ipv4

# Addresses
alias_maps = hash:/etc/postfix/aliases
alias_database = $alias_maps
recipient_delimiter = +

# I'm the only user on my machine, so I send from whichever address I want.
#smtpd_sender_login_maps = hash:/etc/postfix/sender_login_maps
#smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch

# spam
#in_flow_delay = 1s
header_checks = regexp:/etc/postfix/header_checks
setgid_group = postdrop

# TLS
smtpd_tls_cert_file = /etc/ssl/tls/mx.idx.cy.crt
smtpd_tls_key_file = /etc/ssl/tls/mx.idx.cy.key
smtpd_tls_security_level = encrypt
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_security_level = encrypt
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

# DKIM
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
milter_default_action = accept

TLS

You will also need an SSL certificate. I hate getting and renewing SSL certificates, because the tools are bulky and automation is yet another moving part in your system (I used the lego package, with the manual DNS challenge for simplicity, but I’m not too happy about it). I won’t give you a tutorial on getting SSL certificates, but note that you don’t have to get and renew a certificate for each of your custom domains!

You just need one SSL certificate for your machine to encrypt data in transit to other SMTP servers. If you create an MX record mx.example.com pointing to your email machine’s IP address, then grab a free certificate for mx.example.com from Let’s Encrypt. Then point to it in the Postfix configuration, and you’ve got transport encryption! In short, only the MX hostname needs a cert for STARTTLS to be used for encryption.

Why no certificates for your actual email domains like example.com? Because the email domain has little to do with transport encryption. TLS only secures the connection between servers. You can still set whatever you want in the From header.

DKIM, SPF, and DMARC

You should prove that your emails actually come from your domain to make your mail trustworthy and deliver to Gmail and co. That’s what DKIM is for, and fortunately it’s a one-time deal per email domain. First you generate a key pair for each domain with OpenDKIM, and then you publish the public key in a TXT record in DNS. The keys don’t expire automatically, but it’s best practice to rotate them periodically. My config uses a naming scheme that allows smooth rotation, but it doesn’t complicate things if you skip it.

There are two more TXT records that you need to publish in DNS: the SPF and DMARC records. You say which hosts are allowed to send mail from your email domain, and give instructions to other email servers about what to do with mail that fails DKIM checks. In my case I told others to reject mail that can’t be verified as coming from my domains, and send reports to my postmaster address.

Take a look at my OpenDKIM config to understand how things come together.

/etc/opendkim.conf
UserID             opendkim:opendkim
Socket             inet:8891@localhost
KeyTable           refile:/etc/opendkim/KeyTable
SigningTable       refile:/etc/opendkim/SigningTable
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts      refile:/etc/opendkim/TrustedHosts

Canonicalization   relaxed/relaxed
ReportAddress      postmaster@idx.cy
SendReports        no

LogWhy             yes
Syslog             yes
SyslogSuccess      no
/etc/opendkim/KeyTable
key1._domainkey.idx.cy         idx.cy:key1:/etc/opendkim/keys/idx.cy/key1.private
key1._domainkey.maxadamski.com maxadamski.com:key1:/etc/opendkim/keys/maxadamski.com/key1.private
key1._domainkey.localchat.cc   localchat.cc:key1:/etc/opendkim/keys/localchat.cc/key1.private
/etc/opendkim/SigningTable
*@idx.cy         key1._domainkey.idx.cy
*@maxadamski.com key1._domainkey.maxadamski.com
*@localchat.cc   key1._domainkey.localchat.cc
/etc/opendkim/TrustedHosts
127.0.0.1
localhost

I generate DKIM keys with the following command:

opendkim-genkey -D /etc/opendkim/keys/example.com -d example.com -s key1

And for each email domain I have the following records in DNS:

TypeNameValue
MXexample.commx.idx.cy
TXTexample.comv=spf1 mx a -all
TXTkey1._domainkeyv=DKIM1; k=rsa; s=email; p=<public-key>
TXT_dmarcv=DMARC1; p=reject; rua=mailto:postmaster@idx.cy

Reverse DNS

One more thing about email trust. I've read that reverse DNS (PTR record) will boost the reputation of your email server. The thing is that your ISP has to set it up, and I suspect my ISP to reply with a polite "no", so I didn't do it yet. As you'll see in the next section, my email gets delivered to Gmail just fine. I also tested with GMX, but I don't know about Microsoft/Yahoo/Proton.

However, if you want wide deliverability, PTR isn't optional.

Testing with Gmail

To try it out, let's send a test mail to Gmail with the sendmail command:

sendmail -vt < test.mail
test.mail
Content-Type: text/html
From: max@idx.cy
To: myaddress@gmail.com
Subject: DKIM test
Test message from idx.cy!

I got the mail instantly and Gmail confirmed TLS encryption.

Click "Show original" in Gmail to see the raw mail. There's lots of text in the headers, so let's just focus on passing SPF, DKIM, and DMARC :)

You'll also get a mail with a report because of the -v option. I receive mail with Heirloom Mail like this:

You have new mail in /var/mail/max
fool ~ | mailx
Heirloom Mail version 12.5 7/5/10.  Type ? for help.
"/var/mail/max": 1 message 1 new
>N  1 Mail Delivery System  Sat Oct  4 15:40  74/2437  "Mail Delivery Status Report"

I use the p command to print the mail.

& p
Message  1:
From MAILER-DAEMON  Sat Oct  4 15:40:50 2025
X-Original-To: max@idx.cy
Delivered-To: max@idx.cy
Date: Sat,  4 Oct 2025 15:40:50 +0200 (CEST)
From: Mail Delivery System <MAILER-DAEMON@idx.cy>
Subject: Mail Delivery Status Report
To: max@idx.cy
Auto-Submitted: auto-replied
Content-Type: multipart/report; report-type=delivery-status;
	boundary="3C311BFF8D.1759585250/mx.idx.cy"
Status: R

Part 1:
Content-Description: Notification
Content-Type: text/plain; charset=utf-8

This is the mail system at host mx.idx.cy.

Enclosed is the mail delivery report that you requested.

                   The mail system

<myaddress@gmail.com>: delivery via
    gmail-smtp-in.l.google.com[X.X.X.X]:25: 250 2.0.0 OK  1759585250
    4fb4d7f45d1cf-6393b6ba951si3187039a12.40 - gsmtp

Great! Everything is working.

If something isn't working for you, please double-check your DNS records, and triple-check that TLS certificates are readable by the Postfix user, and that DKIM keys are readable by the OpenDKIM user. Postfix and OpenDKIM logs will also be useful. The OpenDKIM config file is especially unforgiving of typos, so watch out for small mistakes!

Next steps

In my next post on email, I'll show you how to use Python to build useful email applications. Thanks for reading!

Btw, if you notice anything about my config just let me know by sending an email to max@idx.cy ;)