DEV Community

Amritasha Agrawal
Amritasha Agrawal

Posted on

Self-Hosting Listmonk on Railway: The Newsletter Server That Can't Send Email on the Wrong Plan

Listmonk is an open-source newsletter and mailing-list manager: subscriber lists, segmentation with SQL, campaign templates, analytics, and transactional mail over an API. It's a single Go binary backed by Postgres, it idles at a few tens of megabytes of RAM, and it's free — the only per-email cost is whichever mail provider actually delivers your messages.

That combination makes it one of the cheapest things you can self-host. It also means the things that go wrong are not "it crashed" but "it's running and nothing is being delivered", which is worse.

This guide covers running Listmonk on Railway, a usage-billed container platform with managed Postgres and volumes. Most of it applies to any container host.

How Listmonk sends mail

Listmonk doesn't deliver email itself. It renders each message and hands it to an SMTP server — Amazon SES, Postmark, SendGrid, Mailgun, Brevo, your own Postfix — through a connection pool you configure in Settings → SMTP. You can register several SMTP servers and Listmonk spreads the load across them.

It also has messengers, which POST each message as JSON to a web service of your choice. Those are for SMS, push notifications and similar channels; email still goes over SMTP.

That's the background for the first decision.

Decision 1: Your plan has to allow outbound SMTP

Railway blocks outbound SMTP (ports 25, 465, 587 and 2525) on its Free, Trial and Hobby plans. It's a platform-wide anti-spam policy, documented under outbound networking, and it can't be lifted per project.

For most apps that's an inconvenience — no password-reset emails. For Listmonk it's the whole product: on those plans, campaigns start, sit in "running", and every message fails to connect. Listmonk's own docs mention that some hosts block SMTP ports; Railway is one of them below the Pro plan.

So:

  • Production newsletters need Railway Pro (or any host that allows outbound SMTP).
  • Hobby is still fine for evaluation: building lists, importing subscribers, designing templates, testing the API. Just don't expect a test campaign to arrive.

Check this before you import a single subscriber.

Decision 2: Upgrades need --upgrade, and a backup first

Listmonk keeps its schema version in the database. A new release ships migrations, applied by running the binary with --upgrade. The official Docker Compose file runs three commands in sequence on every start:

./listmonk --install --idempotent --yes   # create tables if this is a new DB
./listmonk --upgrade --yes                # apply any pending migrations
./listmonk                                # start the server
Enter fullscreen mode Exit fullscreen mode

If your container only runs the first and last, a new image version will boot against an old schema and refuse to start until someone runs the upgrade by hand. Two more rules from Listmonk's upgrade guide:

  • Back up Postgres before every upgrade. Upgrades are idempotent, but downgrades are not supported: going back means restoring the pre-upgrade database.
  • Pin the image tag. listmonk/listmonk:latest means a redeploy you did for an unrelated reason can pull a new major version. Pin a release (listmonk/listmonk:v6.2.0 at the time of writing) and upgrade deliberately.

Decision 3: Where uploaded images live

Images you upload for campaigns go through Settings → Media. The default backend is the filesystem, under Listmonk's uploads directory — inside the container. On any container platform that directory is wiped on every redeploy, and every newsletter you've already sent now has broken images in it.

Two correct setups:

  • A volume mounted at the uploads path (/listmonk/uploads in the official image), with the media settings pointing there.
  • S3-compatible storage, configured in the same settings page. This is the better choice once you're sending to real audiences, because images are served by the bucket rather than by your Listmonk instance — a large campaign means thousands of image requests within minutes of sending.

Decide before your first campaign; switching later doesn't move old files.

Decision 4: Bounces, or your sender reputation pays

Every provider tracks your bounce and complaint rates and throttles or suspends you if they climb. Listmonk can mark bounced addresses and stop mailing them, but only if you turn it on (Settings → Bounces) and give it a source:

  • Provider webhooks — the cleanest option. Listmonk has built-in endpoints for Amazon SES, SendGrid, Postmark, Forward Email and Lettermint at /webhooks/service/<provider>. Your Listmonk URL has to be publicly reachable for the provider to call it.
  • A POP3 bounce mailbox that Listmonk scans. Use a dedicated mailbox: Listmonk deletes the messages it processes.
  • The generic bounce API (POST /webhooks/bounce) for anything else.

If you only set up one thing beyond SMTP, make it this.

Configuration on a container platform

Listmonk reads its config file, but every key can also come from an environment variable: LISTMONK_ plus the section and key, joined by a double underscore. The ones you need on Railway:

LISTMONK_app__address=0.0.0.0:9000
LISTMONK_db__host=<postgres private host>
LISTMONK_db__port=5432
LISTMONK_db__user=<user>
LISTMONK_db__password=<password>
LISTMONK_db__database=<database>
Enter fullscreen mode Exit fullscreen mode

Set them as references to the Postgres service so they follow it automatically, and keep the connection on Railway's private network.

The first admin account is created on install. Set LISTMONK_ADMIN_USER and LISTMONK_ADMIN_PASSWORD before the first boot and that becomes the Super Admin; leave them unset and Listmonk asks you to create one in the browser — in which case, open the URL immediately, before anyone else does.

Everything else — SMTP servers, media, bounces, the public root URL used in unsubscribe and tracking links — lives in Settings and is stored in the database, so it survives redeploys without more environment variables. Set Settings → General → Root URL to your public URL before sending anything: it's baked into every unsubscribe link.

Deploying it

I maintain a Railway template with Listmonk and Railway Postgres 18 wired together over the private network, an admin account created from variables on the first boot, and a health check on /health.

Deploy on Railway

(Disclosure: I earn a share of Railway usage from deployments of this template. The source is public at Amritasha/listmonk, and everything here applies if you set it up yourself.)

After the deploy goes green:

  1. Log in with the admin user and password from the service variables, and change the password.
  2. Settings → General: set the Root URL to your public domain.
  3. Settings → Media: add a volume at the uploads path or configure S3 before uploading anything.
  4. Settings → SMTP: add your provider (on a plan that allows SMTP), then send a test to yourself.
  5. Settings → Bounces: enable it and wire your provider's webhook.
  6. Import a small list, send a real campaign to yourself and one other address, and check the unsubscribe link works.

Keeping it fast as lists grow

Listmonk is quick until the subscriber, view and click tables get large. Then admin pages that count and aggregate (subscriber totals, campaign stats) start taking seconds. Two built-in fixes from the maintenance docs:

  • Settings → Performance → Cache slow database queries — recommended once those pages stop loading instantly.
  • VACUUM ANALYZE weekly on the Postgres database. It's briefly blocking on a big database, so schedule it outside sending windows.

Backups

Everything — subscribers, consent records, campaigns, settings — is in Postgres:

  • Railway volume backups. The Postgres volume's Backups tab offers daily (kept 6 days), weekly (kept 27 days) and monthly (kept 89 days) schedules. Turn them on.
  • An off-platform copy, because Railway backups restore only inside the same project:
  pg_dump "$DATABASE_PUBLIC_URL" -Fc -f listmonk-$(date +%F).dump
Enter fullscreen mode Exit fullscreen mode
  • Always a fresh dump right before an upgrade — it's the only downgrade path.

Your subscriber list is personal data. Treat the dumps accordingly: encrypted storage, limited access, and a retention period.

What it costs

Railway bills per second: about $10 per GB of RAM per month, $20 per vCPU per month, and $0.16 per GB-month of volume storage. Hobby is $5/month including $5 of usage; Pro is $20/month including $20.

Listmonk itself is tiny at idle; Postgres is usually the bigger of the two. Sending a large campaign briefly uses more CPU, and images served from the instance add egress, which is another reason to put media in a bucket. Delivery costs are separate and charged by your mail provider. After a week, each service's Metrics tab shows the real numbers.

When not to self-host it

  • You don't want to own deliverability. SPF, DKIM, DMARC, bounce handling and complaint rates are your job now. Hosted newsletter tools do that for you.
  • You won't run a Pro-level plan or an SMTP-friendly host. Without outbound SMTP, Listmonk can't send.
  • Your list is small and your time isn't. Free tiers of hosted tools may cover you.

Checklist

  • [ ] Plan allows outbound SMTP (Railway Pro or higher)
  • [ ] Image pinned; start command runs --install --idempotent and --upgrade
  • [ ] Root URL set before the first campaign
  • [ ] Media on a volume or S3
  • [ ] SMTP provider configured and test email received
  • [ ] Bounce processing enabled with a provider webhook or dedicated POP3 mailbox
  • [ ] SPF, DKIM and DMARC set up for your sending domain
  • [ ] Volume backups + off-platform pg_dump; fresh dump before each upgrade
  • [ ] "Cache slow database queries" + weekly VACUUM ANALYZE once lists get big

If something here is wrong or out of date, say so in the comments — I'll fix it.

Top comments (0)