Introduction Self-hosting an email server is useful for automating tasks like mailing lists, newsletters, or email verification APIs. The elephant in the room is real-world deliverability. With self-hosting you risk not receiving mail or someone missing your mail. I accept this for my personal projects, but you may not. Keep this in mind. For me the 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. I've been semi-comfortably using mailx for a month already (with its ancient user interface!), so the setup is enough for me now, but I could expand it in the future, and multi-user webmail isn’t completely off the table. 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. 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 (strict) 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 /etc/postfix/master.cf # Postfix master process configuration file. For details on the format # of the file, see the master(5) manual page (command: "man 5 master" or # on-line: http://www.postfix.org/master.5.html). # # Do not forget to execute "postfix reload" after editing this file. # # ========================================================================== # service type private unpriv chroot wakeup maxproc command + args # (yes) (yes) (no) (never) (100) # ========================================================================== smtp inet n - n - - smtpd pickup unix n - n 60 1 pickup cleanup unix n - n - 0 cleanup qmgr unix n - n 300 1 qmgr #qmgr unix n - n 300 1 oqmgr tlsmgr unix - - n 1000? 1 tlsmgr rewrite unix - - n - - trivial-rewrite bounce unix - - n - 0 bounce defer unix - - n - 0 bounce trace unix - - n - 0 bounce verify unix - - n - 1 verify flush unix n - n 1000? 0 flush proxymap unix - - n - - proxymap proxywrite unix - - n - 1 proxymap smtp unix - - n - - smtp relay unix - - n - - smtp -o syslog_name=postfix/$service_name # -o smtp_helo_timeout=5 -o smtp_connect_timeout=5 showq unix n - n - - showq error unix - - n - - error retry unix - - n - - error discard unix - - n - - discard local unix - n n - - local virtual unix - n n - - virtual lmtp unix - - n - - lmtp anvil unix - - n - 1 anvil scache unix - - n - 1 scache postlog unix-dgram n - n - 1 postlogd Notice that there's no mention of POP3 or IMAP! I did waste some time trying to set them up with Dovecot (because they changed their config format too much, so guides became outdated, and their web docs were just hard to read for me). Ultimately I can just SSH to my server and I feel comfortable with mailx, so I skipped Dovecot. One package less in my system :) TLS You will also need an SSL certificate for encryption in transit. 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 A 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 [email protected] 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: Type Name Value MX example.com mx.idx.cy TXT example.com v=spf1 mx a -all TXT key1._domainkey v=DKIM1; k=rsa; s=email; p= TXT _dmarc v=DMARC1; p=reject; rua=mailto:[email protected] Reverse DNS One more thing about self-hosted email deliverability. 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. GMX and Outlook also didn't mark my mail as spam. Maybe my IP is lucky :) But in general, 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: [email protected] To: [email protected] 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: [email protected] Delivered-To: [email protected] Date: Sat, 4 Oct 2025 15:40:50 +0200 (CEST) From: Mail Delivery System Subject: Mail Delivery Status Report To: [email protected] 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 : 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 (or want to share some thoughts) just email me at [email protected] :)