No game code today. Instead we stood up the entire mailing list infrastructure for the studio. Mailchimp charges $13/month for 500 subscribers and scales to "please contact sales" territory fast. We're a lean studio. We self-host.

The stack

Four services, all running on the same DigitalOcean droplet that serves this website:

  • Listmonk v6.0.0 — open-source mailing list manager. Handles subscribers, campaigns, templates, analytics. One 19MB binary, ~25MB RAM at runtime.
  • PostgreSQL 16 — Listmonk's database backend.
  • Postfix — SMTP server for actually delivering the emails. Configured send-only: it only accepts mail from localhost (Listmonk), nothing inbound.
  • OpenDKIM — signs every outbound email with a 2048-bit DKIM key so Gmail and Outlook don't immediately trash them.

Nginx reverse-proxies the Listmonk admin UI at lists.alpharabbit.purefictionrecords.com. SSL via Let's Encrypt once DNS propagates.

Why self-host email

The same reason we self-host everything else: control and margins. Third-party email services charge per subscriber, per send, or both. At scale those costs compound. A self-hosted stack costs nothing beyond the server we're already paying for.

The trade-off is setup complexity and deliverability management. You have to configure SPF, DKIM, and DMARC correctly or your emails go straight to spam. You have to manage your sending IP's reputation. There's no "it just works" button. But the setup is a one-time cost, and the ongoing maintenance is minimal once the DNS records are in place and the IP is warm.

The deliverability stack

Sending email is easy. Getting it into inboxes is the actual problem. Three DNS records handle authentication:

  • SPF — a TXT record that says "only this IP address is allowed to send email for purefictionrecords.com." If an email claims to be from us but comes from a different server, receiving mail servers know it's forged.
  • DKIM — OpenDKIM attaches a cryptographic signature to every outbound email. The receiving server checks it against a public key published in DNS. If the signature doesn't match, the email was tampered with in transit (or never came from us at all).
  • DMARC — ties SPF and DKIM together with a policy. We're starting with p=none (monitor mode) — receiving servers report authentication failures but don't reject anything yet. Once we verify everything is signing correctly, we'll tighten it to p=reject.

The port 25 problem

Here's something that isn't obvious until you hit it: DigitalOcean blocks outbound port 25 on all droplets by default. This is the port SMTP uses to deliver mail to other servers. It's blocked at the network/hypervisor level — not a firewall you can configure. Every droplet ships this way to prevent compromised instances from being used as spam relays.

To unblock it you submit a support ticket explaining what you're doing and that you have proper authentication (SPF/DKIM/DMARC) configured. They review it and flip a switch on their end. Reasonable policy, mild inconvenience.

Until that ticket is resolved, the entire stack is installed and configured but can't actually deliver mail to the outside world. Everything else works — Listmonk runs, Postfix accepts mail locally, OpenDKIM signs it — the packets just get dropped at the border.

Fitting on a small droplet

This is a 1.9GB RAM droplet. It was already running Nginx for the static site. After adding PostgreSQL, Listmonk, Postfix, and OpenDKIM:

  • Total RAM used: ~800MB of 1968MB
  • Listmonk: ~25MB
  • OpenDKIM: ~2.5MB
  • PostgreSQL: shared buffers configured at 128MB

We went with a native binary install instead of Docker specifically because Docker's overhead would eat into the headroom on a machine this size. The Listmonk binary is a single file. No containers, no orchestration, no abstraction layers consuming RAM for the privilege of existing.

What's left

Three things that can't be done from the server itself:

  1. DNS records — four TXT/A records need to be added at the domain registrar. The server can generate DKIM keys but it can't publish its own DNS.
  2. SSL certificate — Certbot needs the A record to be live before it can verify domain ownership and issue a cert.
  3. Port 25 unblock — waiting on DigitalOcean support.

Once those land, we send a test email, verify the DKIM signature with an online checker, and start building the subscriber list. The plan is to use it for game launch announcements, devlog digests, and playtest invitations.

Back to dungeon crawling tomorrow.