JustEmails
Start free trial
Tutorials··12 min read

Discourse SMTP Setup: 6 Steps to Outbound and Reply-by-Email

The full Discourse SMTP setup in app.yml — DISCOURSE_SMTP_ vars, notification vs reply by email address, POP3 polling for replies, and the bounce setting nobody touches until the first digest.

By JustEmails Platform Team
Contents
  1. What We're Building
  2. Prerequisites
  3. Step 1: Two Mailboxes, Two Different Jobs
  4. Step 2: The `DISCOURSE_SMTP_` Block in `app.yml`
  5. Step 3: Rebuild, Don't Restart
  6. Step 4: Turn On Reply-by-Email
  7. Step 5: Bounce Handling — Before the First Digest
  8. Step 6: Test It
  9. Common Discourse SMTP Errors and Fixes
  10. Next Steps
  11. Frequently Asked Questions
  12. Do I have to rebuild Discourse after changing SMTP settings?
  13. What's the difference between notification email and reply by email address in Discourse?
  14. Does Discourse reply-by-email need the separate mail-receiver container?
  15. Why are my Discourse digest emails landing in spam?
  16. Try JustEmails

Sunday, 9:02 AM. The first weekly digest fires on a forum I'd stood up for a community of about 600 people — Discourse SMTP freshly configured, DNS barely warm — and my phone starts buzzing with the same question from three different moderators: did we just get hacked?

We hadn't. Discourse did exactly what I'd configured it to do — queue 600 digest emails from a nine-day-old domain, out of a noreply@ address with no DKIM key, in one Sidekiq burst. Gmail took maybe ninety seconds to form an opinion. And everything Discourse sent afterward — password resets, watched-topic alerts, moderator flags — inherited the hole for three weeks.

Discourse is the most mail-dependent thing I run. Not close. A wiki that can't send email has a broken feature; a forum that can't send email is a forum nobody comes back to, because the whole re-engagement loop is notifications.

So here's the whole Discourse SMTP setup, outbound and inbound, including the bounce settings you won't think about until the day you need them.

What We're Building

A self-hosted Discourse instance that sends notification and digest mail through authenticated SMTP on JustEmails, signed with DKIM — plus working reply-by-email, so a member can answer a notification from their phone without opening the forum.

By the end:

  • Digests and notifications arriving with spf=pass dkim=pass dmarc=pass
  • Password resets that land in under a minute
  • Replies to notification emails posting back into the right topic
  • Bounces feeding Discourse's bounce score instead of vanishing

Two mailboxes, one env block, two site settings. The env block is the easy part.

Prerequisites

  • A standard Discourse install — the Docker one, /var/discourse, driven by ./launcher. Other install methods use the same settings in a different file.
  • Root shell on the forum host.
  • A JustEmails account with your domain added and DNS showing green.
  • Outbound 587 open. Test first: nc -zv smtp.justemails.app 587 from the forum host. Most providers block 25 by default and a few block 587 too — something you find out at midnight with a config you know is right. Ports 587, 465 and 995 each want a different TLS mode, which the port breakdown covers.

Step 1: Two Mailboxes, Two Different Jobs

Make both of these in your JustEmails dashboard before you touch a config file:

MailboxDiscourse settingDirection
forum@yourdomain.comDISCOURSE_NOTIFICATION_EMAILOutbound — the From on everything
replies@yourdomain.comreply by email addressInbound — polled for replies and bounces

Not interchangeable. Outbound is authenticated SMTP with the notification address as sender; inbound reply mail lands in the replies mailbox carrying a key that tells Discourse which post it belongs to. Point both at one box and things sort of work — right up until someone replies to a digest and Discourse starts reading its own outbound mail. Which is a fun morning.

Mailboxes are unlimited on the single $49/year plan, so two costs nothing and keeps the credentials separate. Make a third if you want somewhere to test.

One opinion, and plenty of people run it the other way: don't name the outbound one noreply@. People reply to forum email, and noreply@ throws those replies away — the member who wrote three paragraphs on their phone never learns it went nowhere. We've argued the general case elsewhere; on a forum it's sharper, because replying is the product.

Step 2: The DISCOURSE_SMTP_ Block in app.yml

Open /var/discourse/containers/app.yml and find the env: section. Add:

env:
  DISCOURSE_HOSTNAME: forum.yourdomain.com
  DISCOURSE_DEVELOPER_EMAILS: 'you@yourdomain.com'

  DISCOURSE_SMTP_ADDRESS: smtp.justemails.app
  DISCOURSE_SMTP_PORT: 587
  DISCOURSE_SMTP_USER_NAME: forum@yourdomain.com
  DISCOURSE_SMTP_PASSWORD: "your-mailbox-password"
  DISCOURSE_SMTP_ENABLE_START_TLS: true
  DISCOURSE_SMTP_DOMAIN: yourdomain.com
  DISCOURSE_NOTIFICATION_EMAIL: forum@yourdomain.com

Four things in there bite people.

Quote the password. Always. An unquoted YAML value containing # truncates there — everything after it becomes a comment, and the failure surfaces as an authentication error, which sends you hunting in entirely the wrong half of the problem. Lost most of an evening to this once. Still quote by reflex.

DISCOURSE_SMTP_DOMAIN is the HELO domain, not the mail domain and not the forum hostname — it's what Discourse announces itself as when the SMTP conversation opens. Your registered domain is the right answer; a container ID is not.

ENABLE_START_TLS on 587, FORCE_TLS on 465. Port 587 connects in the clear and upgrades; 465 is wrapped in TLS from the first byte. Set DISCOURSE_SMTP_FORCE_TLS: true while pointing at 587 and you get a handshake that hangs until timeout with no useful error.

Authentication defaults to plain. If auth fails on a password you know is right, try DISCOURSE_SMTP_AUTHENTICATION: login before blaming the credentials.

You'll also find DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: none in a hundred forum threads. Don't. It "fixes" certificate errors by not checking certificates, which turns your encrypted connection into a costume. Cert errors mean the container is missing a CA bundle — fix that instead.

Step 3: Rebuild, Don't Restart

cd /var/discourse
./launcher rebuild app

restart is not enough. The env: block gets baked into the container at bootstrap, so restarting brings the old values straight back and you'll be certain the file didn't save. Rebuild takes a few minutes and the forum is down for most of it — pick a quiet hour.

Anything set through app.yml also overrides the matching field in the admin UI and shows up locked. Which is why half the "I changed the setting and nothing happened" threads end at "it's in your env."

If the rebuild fails, /var/discourse/discourse-doctor prints a diagnostic dump and offers to walk you back through the mail settings. First thing I run now, instead of the last.

Step 4: Turn On Reply-by-Email

This is where most setups stop half-finished. Outbound works, everyone declares victory, inbound never gets configured.

Add the polling block to app.yml and rebuild again (sorry):

  DISCOURSE_POP3_POLLING_ENABLED: true
  DISCOURSE_POP3_POLLING_SSL: true
  DISCOURSE_POP3_POLLING_HOST: mail.justemails.app
  DISCOURSE_POP3_POLLING_PORT: 995
  DISCOURSE_POP3_POLLING_USERNAME: replies@yourdomain.com
  DISCOURSE_POP3_POLLING_PASSWORD: "your-replies-mailbox-password"

Then in Admin → Settings → Email, flip reply by email enabled and set:

replies+%{reply_key}@yourdomain.com

That %{reply_key} is a literal placeholder — Discourse substitutes a unique token per notification, and that token is how a reply finds its topic. So your host has to deliver plus-addressed mail to the base mailbox. JustEmails does; if you'd rather not depend on it, a catch-all on the domain gets you the same result, and plus addressing in general is worth understanding before you debug this at 1 AM.

Polling runs every 5 minutes on a stock install. Near-real-time, not real-time.

Which brings me to the contrarian bit: skip the mail-receiver container. Most guides route you toward it, and it's right when mail is delivered straight to your host or you run your own MX at volume. On hosted mailboxes it's a second container to patch, monitor and break during upgrades — to shave a few minutes off reply latency. Nobody has ever noticed a four-minute reply delay. Somebody will absolutely notice when a container silently stops.

Step 5: Bounce Handling — Before the First Digest

Discourse uses VERP for bounce attribution: the Return-Path carries an encoded key, so a returning bounce names exactly which user and message it belongs to. Envelope sender and header sender doing two unrelated jobs — the Return-Path versus From split, in the wild.

The catch: that VERP address is built from the bounce email prefix site setting, which defaults to noreply. So bounces come back to noreply+verp-<key>@yourdomain.com while your poller watches replies@. Different local parts. Bounces land nowhere, the bounce score never moves, and the forum keeps cheerfully mailing dead addresses for months.

One setting fixes it: change bounce email prefix to replies so bounce mail and reply mail share the polled mailbox. (A catch-all covers both. I'd still set the prefix explicitly — future-you won't remember why it depended on the catch-all.)

Once bounces arrive, the scoring defaults matter. Hard bounce adds 2 to a user's score, soft adds 1, and at a threshold of 4 Discourse stops emailing that account. Scores reset after 30 days. Leave those alone — the failure mode is never "too strict," it's a poller nobody wired up.

While you're on that page: digest frequency defaults to weekly, which means one queued message per eligible member, all at once. On a young domain that's the likeliest thing to hurt you. Lowering suppress digest email after days trims dormant accounts out of the send list without touching anyone who's actually reading.

Step 6: Test It

From the host:

cd /var/discourse
./launcher enter app
rake emails:test[you@yourdomain.com]

rake emails:test is genuinely good — it opens a real SMTP connection, reports what it negotiated, and flags misconfigurations in plain language instead of a Ruby backtrace. Run it first.

When the test lands, open the raw headers:

Authentication-Results: ...
  spf=pass
  dkim=pass
  dmarc=pass

All three should say pass. JustEmails auto-configures the SPF, DKIM, DMARC and MTA-STS records, but they still have to be live in DNS before a receiver can verify anything — so dkim=none on a domain you added twenty minutes ago is propagation, not config. Give it an hour.

Then go to Admin → Email. Five tabs, and the one everybody ignores is Skipped — it logs every message Discourse decided not to send, and why. "User is already reading the topic." "Email disabled for this user." Those explain most "notifications aren't working" reports, and none of them are SMTP problems.

For inbound: reply to any notification email from an address that matches a forum account. It should appear in the topic within five minutes. If it doesn't, the Rejected tab names the parse or destination check that failed. Read that before you touch the config. I never do.

Common Discourse SMTP Errors and Fixes

Net::SMTPAuthenticationError: 535 5.7.8 Authentication credentials invalid

Check the password for a # first. Always the password. Then confirm DISCOURSE_SMTP_USER_NAME is the full address and not the local part — I have typed forum where forum@yourdomain.com belonged more times than I'd like to put in writing, and the 535 reads exactly the same either way. If both check out, try DISCOURSE_SMTP_AUTHENTICATION: login.

Net::OpenTimeout: execution expired

Nothing wrong with the config — the host is blocking outbound. Run the nc -zv check from Prerequisites. Providers block 25 on new accounts almost universally, and a few quietly filter 587 as well, which is the one that stings, because every guide you'll read tells you 587 is the safe port. It is. Unless your host disagrees, and then nobody tells you.

Net::SMTPFatalError: 550 sender address not verified

Auth succeeded, then the relay refused the message because DISCOURSE_NOTIFICATION_EMAIL sits on a domain that isn't verified on your account. Two checkpoints, two failures, and the error only mentions the second one.

Outbound works, replies never post

In order: POP3 polling enabled in the env, reply by email address contains %{reply_key}, the replies mailbox accepts plus-addressed mail, and the replying address belongs to a real forum account. Then read the Rejected tab, which knew all along.

Digests stop reaching one member

Their bounce score crossed the threshold. Admin → Email → Bounced, reset it — then go check Step 5 was ever finished, because a score that moves at all means the poller works.

Next Steps

Three things I'd do next:

  • Watch Sidekiq. /sidekiq shows the mail queue live. An unreachable relay makes Discourse retry rather than error loudly, so a growing queue is your first real signal something broke.
  • Point your other self-hosted services at the same domain. Uptime Kuma alerts take the same host and port from a different settings page.
  • Track what forum traffic actually does with JustAnalytics, so you can tell whether that digest brings people back or just burns sender reputation once a week.

Frequently Asked Questions

Do I have to rebuild Discourse after changing SMTP settings?

If you changed app.yml, yes — run ./launcher rebuild app. The env block is baked into the container at bootstrap, so a restart brings the old values back up and you'll swear the edit didn't save. Settings changed in Admin → Settings → Email apply immediately, which is why reply by email enabled lives there and the SMTP credentials don't.

What's the difference between notification email and reply by email address in Discourse?

Notification email is the outbound From on everything Discourse sends — digests, watched-topic alerts, password resets. Reply by email address is inbound only: it carries the reply key that maps a reply back to the right post, so it looks like replies+%{reply_key}@forum.example.com and has to point at a mailbox you poll. Setting them to the same address works right up until someone hits reply on a digest.

Does Discourse reply-by-email need the separate mail-receiver container?

No. The mail-receiver container is for setups where mail is delivered straight to the Discourse host and handed to the API — useful at scale or when you run your own MX. On hosted mailboxes, POP3 polling does the same job with one less container to patch. The tradeoff is latency: polling runs every 5 minutes by default.

Why are my Discourse digest emails landing in spam?

Usually volume shape, not content. A weekly digest queues one message per eligible member in a single Sidekiq burst, so a young domain that sent nine messages all week suddenly sends nine hundred at once — that pattern alone gets filtered. Confirm SPF, DKIM and DMARC pass on a test send, then shrink the blast: raise the digest interval, or lower suppress digest email after days.


Try JustEmails

Unlimited custom domain email hosting for $49/year flat — unlimited domains, unlimited mailboxes, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone managing email across more than one domain.

Start your 7-day free trial → · How it compares

discourse-smtpreply-by-emailpop3-pollingself-hosted-forumapp-yml-configbuildinpublicsaasstudioaiworkforcebuildwithclaude

Related posts

Tutorials
Zabbix and Netdata Alert Delivery Using JustEmails SMTP
12 min read
Tutorials
Jenkins SMTP Setup: Build Notification Emails via JustEmails
14 min read
Tutorials
Nextcloud SMTP Settings: Step-by-Step JustEmails Setup
13 min read