Self-Hosting a Mailing List with Hugo and Listmonk

I've been meaning for a while to fix a basic issue with my blog: there's no mailing list. Or at least there wasn't until today! I've just deployed one and you can subscribe here. I promise I'll write more than one post a year, but not enough for it to get annoying 😄.

There are a lot of ways to set up a mailing list, many of which are easy, but where's the fun in that! I decided to do it the hard way and self-host as much as possible.

Self-Hosting Email Is Hard

Thanks to the enduring legacy of the spam wars, it's extremely difficult nowadays to send email from servers you control. If your IP ends up on a blocklist you can be banned from sending email indefinitely, and all residential IPs (like my home internet connection) are blocked by basically all email providers. So full self-hosting is impossible unless you have a dedicated non-residential IP address with a good email address (which I don't) or use some sort of managed service.

The sanest option is to use a specialized service like Mailchimp to send out newsletters, but I wanted to self-host what I could even if I couldn't self-host everything.

Doing It My Way

The tech stack I ended up with is:

  • Listmonk running on my Kubernetes cluster for list management
  • AWS SES for sending the actual emails
  • A little Go program I hacked up for creating an email campaign every time I publish a blog post
Obligatory architecture diagram

Listmonk: The Brains of the Operation

Listmonk is basically an open source version of Mailchimp. It manages subscription lists, email campaigns (which is jargon for an email you send to a lot of people), templates, etc. Listmonk also hosts the subscription page, which I expose to the internet through Cloudflare Tunnels (see my post on how I host this blog for more details on the setup). Listmonk requires a Postgres database (which I deploy with the Bitnami Helm chart) and some S3-style storage for static assets (which I'm using Ceph Object Gateway for), but other than that it's straightforward to deploy on Kubernetes.

I've been really impressed with Listmonk so far: it's got a nice UI and has all the features I need for my (fairly simple) use-case, including a decent API. We'll see how it holds up under real-world use!

SES: Actually Sending the Emails

The only thing Listmonk doesn't do is send the actual emails: it just hands them over to an SMTP relay when they're ready to be delivered. Any SMTP relay could do (including a self-hosted one), but I decided to use SES because it's dirt-cheap at low volume and has some nice bonus features (like bounce processing that integrates with Listmonk).

Setting up SES wasn't trivial—the process for getting SMTP credentials is hilariously convoluted, and as with any email provider you need set up the appropriate SPF and DKIM DNS records—but now that it's working I don't expect to have to do much handholding.

Automating the Campaigns

Of course I wanted to automate the process of generating email campaigns every time I post something. This blog is a static site generated with Hugo and doesn't have an API or anything like that, but it does have an RSS feed that includes all the information I need to generate mail campaigns. So I wrote a small Go program that:

  • Pulls the RSS feed for this blog
  • Compares the RSS entries to a list of already-processed entries (just a simple text file in an S3-style bucket)
  • For any post that hasn't been processed, create an email campaign in Listmonk using the API (and update the processed list)

I also segregate posts based on keywords: everything with the technical keyword goes to the "Technical Posts" mailing list, while other keywords go to the "Everything Else" mailing list. The program runs in a Kubernetes CronJob every half hour. Theoretically I should never have to touch the Listmonk UI again!

Stay Tuned If You're So Inclined

I won't do a hard sell, but if you subscribe to the mailing list you'll be among the first to know about exciting things like:

  • Open sourcing the bucketloads of YAML that I use to manage my homelab
  • Trying out crazy Cilium features I don't need
  • General hot takes about programming (because there aren't enough of those flying around the internet)
  • More!