← Back to Wiki
Self-Hosting / Networking

Send Email From a Custom Domain with Mailgun and Cloudflare DNS

A contact form is a small feature. "Actually deliver the email" is the whole hard part. A residential or cloud-VPS IP has zero sending reputation and gets blocked before a receiving server checks anything else. This wires up Mailgun as the trusted last-mile hop. Plus the three gotchas that ate most of the time. A per-account domain limit. A brand-new account getting auto-disabled. And a dashboard that lies about DNS state for several minutes after it is fixed.

Share on X

Why not just send directly

Sending SMTP straight from your own server to a recipient's mail provider mostly does not work anymore. Residential IP ranges are pre-listed on blocklists like Spamhaus's PBL. Even a clean cloud VPS IP has no track record, and many providers reject the connection before checking SPF, DKIM or DMARC. A transactional email provider solves this by being the thing that has reputation. Mailgun, Postmark, SES. Your server authenticates to them over SMTP and their IP does the real delivery. Your app still owns message content and recipients. The provider is only the trusted final hop.

Gotcha 1: an existing account may already be at its domain limit

BE WARNED: free and lower Mailgun tiers cap the number of sending domains per account, not just messages sent. If you already use Mailgun for something else on the same account, adding a second project's domain fails outright with Domain <name> can't be created. Domains limit of 1 has been exceeded for the account. Upgrade that account's plan, or stand up a genuinely separate account for the new project.

Decide that up front rather than discovering it mid-setup. Check your existing account's plan limits before you assume a new domain will just work.

Gotcha 2: a brand-new account can get auto-disabled before you finish setup

If you go the separate-account route instead of upgrading, know that Mailgun runs real anti-abuse checks on fresh signups. A new account here got flagged and disabled before activation completed, with Your account is temporarily disabled... Please contact support. Most likely tied to something in the phone-verification step. There is no way to self-resolve it. It needs a support ticket, with no guarantee of a fast turnaround. If you hit this, upgrading an existing account is often faster than fighting a fresh one through support.

Adding the sending domain

Once you have an account with room for it, go to the Mailgun dashboard → Sending → Domains → Add new domain. Use a subdomain like mg.yourdomain.com, not the bare root domain. That keeps this separate from any existing mail setup on the root, MX records or a different provider. Mailgun then gives you the exact DNS records to add.

The DNS records, and what each one does

Add these directly in your DNS provider, Cloudflare in this case. Mailgun's "automatic setup" option can log into some providers for you. Doing it by hand avoids granting a new OAuth connection for a one-time task:

Mailgun's generated DMARC record includes a third-party reporting address. inbox.ondmarc.com, their optional OnDMARC integration, alongside their own dmarc.mailgun.org. Trim that out if you would rather not send DMARC aggregate reports to a second company you did not choose. A conservative, monitor-only record with just Mailgun's own address is enough to get real verification working: v=DMARC1; p=none; rua=mailto:<your-mailgun-id>@dmarc.mailgun.org;

None of these need Cloudflare's orange-cloud proxy. Leave every one DNS only. Proxying a CNAME meant for mail infrastructure through Cloudflare's HTTP proxy breaks it, and SPF, DKIM and DMARC TXT records are not proxyable at all.

Gotcha 3: the dashboard's "Unverified" badge lags real DNS by several minutes

BE WARNED: even once the records are correct and live at your DNS provider's authoritative servers, Mailgun's own domain-settings page keeps showing every record Unverified for a while. Clicking "Check status" does not force it to catch up. Do not treat the dashboard as the source of truth mid-setup.
dig +short TXT mg.yourdomain.com
dig +short TXT krs._domainkey.mg.yourdomain.com
dig +short TXT _dmarc.mg.yourdomain.com
dig +short CNAME email.mg.yourdomain.com

If all four resolve correctly against real DNS, the records are fine and the dashboard is slow to notice. A real send attempt is a better signal than the badge. It fails with an explicit Domain ... is not allowed to send: unverified and requires DNS configuration until Mailgun's side catches up, then starts working with no other change.

Creating a scoped SMTP credential

Do not reuse an SMTP login from another project on the same account. Go to the Mailgun dashboard → SMTP CredentialsAdd new SMTP user. Give it a login scoped to what it is for, such as [email protected], and let it auto-generate the password. Then a leaked credential for one project cannot send as another.

smtp.mailgun.org:587 (STARTTLS)
user: [email protected]
pass: <generated>

Verifying end to end

A 200 and {"ok":true} from your own contact-form endpoint proves nothing if that endpoint returns optimistically before confirming delivery. Check the real send result. Your app's own logs, or Mailgun's Logs tab, for a clean accepted or delivered entry. Not an HTTP success from your API. The two failure modes that show up here produce clearly different error text in the logs, so it is easy to tell which gotcha you are looking at. A domain still unverified, or a bad or expired SMTP credential.