We moved our own domains off Resend and Mailgun in August 2026, and moving off Resend turned out to be mostly a configuration change once the server was right. Three production domains now send and receive through one self-hosted Postfix server. The per-email bills went away; the VPS was one we already ran.

This is what we moved, how we did it, what we checked, and the mistakes we made along the way. It's our own infrastructure, so we're describing what we actually did, not a template.

What we moved

Three of our own production domains:

  • a SaaS application,
  • an analytics site,
  • a third business domain.

Before the move, Resend handled outbound mail (signups, notifications, transactional mail) and Mailgun handled inbound (routing addresses on those domains to our real inboxes). After the move, one Postfix server does both jobs.

We're not going to quote old bills, volumes or savings. The honest summary is the one above: the per-email charges stopped, and the server cost nothing extra because we already ran it. Whether a move saves you money depends on your volume; our break-even write-up covers that.

The server

The target was an OVH VPS in Oregon running Ubuntu 26.04 LTS:

Component What it does
Postfix 3.10 Sends and receives mail
OpenDKIM 2.11 Signs outbound mail, 2048-bit RSA, selector mail, one key per domain
postsrsd 2.0 Rewrites envelope senders (SRS) on forwarded mail
Dovecot 2.4 SASL authentication only, no IMAP
fail2ban Bans brute-force attempts on sshd and Postfix SASL
certbot Let's Encrypt certificates, with a deploy hook that reloads Postfix

The server is IPv4 only and has two IPs. Postfix chooses the outbound IP by sender domain using sender_dependent_default_transport_maps, pointing at two smtp transports that each set their own smtp_bind_address and smtp_helo_name. One stream was newer than the other, so we throttled it in Postfix itself:

# main.cf — slow down the cold-ish transport
warmup_destination_concurrency_limit = 1
warmup_destination_rate_delay = 30s

(warmup stands in for whatever you name that transport in master.cf; the setting prefix must match it. One delivery at a time, 30 seconds apart, per destination keeps a new stream from looking like a sudden burst.) The full build is in our Postfix on Ubuntu guide.

How the apps send: submission on 587

The apps don't talk to port 25. They relay through authenticated submission:

  • Port 587 with STARTTLS required.
  • SASL login, backed by Dovecot.
  • smtpd_sender_login_maps plus reject_sender_login_mismatch, so each login can only send as its own address. A leaked credential for one app can't send as another domain.

From the app's point of view, the change from Resend to our server was just SMTP settings: host, port, username, password.

The fallback: a blank variable means roll back

We didn't delete anything at the old providers. Each app reads its SMTP host from an environment variable. If that variable is set, mail goes to our Postfix server. If it's blank, the app uses the old provider exactly as before.

That made rollback a config change, not a deploy. It meant we could switch with very little risk, and it's the first thing we'd recommend to anyone migrating from Resend.

Inbound: replacing Mailgun routes

Mailgun's job was forwarding. We replaced it with Postfix virtual aliases that forward each address to an existing Gmail inbox. Forwarding normally breaks SPF at the destination, because our server isn't in the original sender's SPF record. postsrsd fixes that by rewriting the envelope sender (SRS). DMARC aggregate reports go to a local mailbox on the server instead.

The DNS we published

For each domain:

Record Value
A mail The server's IP
MX mail host
SPF (domain) v=spf1 ip4:<ip> -all
SPF (mail host) v=spf1 a -all
DMARC p=quarantine; rua=...; adkim=s; aspf=s
DKIM TXT at mail._domainkey
PTR Set at the VPS provider

Strict alignment (adkim=s; aspf=s) works because we control every sending path. Our guides on SPF, DKIM and reverse DNS go through each record.

What we verified before trusting it

  • DKIM key: the published TXT record matched the signing key.
  • FCrDNS: the PTR, the forward lookup of that name and the HELO name all agreed.
  • Blocklists: clean, once we discounted the false positives described below.
  • Certificate renewal: a certbot dry run succeeded, and the deploy hook reloads Postfix.
  • End to end: port25's check-auth verifier returned SPF pass, DKIM pass and iprev pass.

Warm-up: authentication isn't reputation

On day one we sent two near-identical test emails to Gmail, one from each of two domains. One landed in the inbox. The other went to spam with Gmail's note that it was "similar to messages identified as spam in the past". SPF, DKIM and DMARC passed for both.

That's what a brand-new or dormant domain looks like: no reputation yet. We kept volume low for about two weeks and sent to engaged recipients. The general rule is tens of emails a day at first, increasing over 2–6 weeks; our IP warm-up guide has a schedule.

Gotchas we actually hit

  1. A doubled DKIM record. A DKIM TXT value was pasted twice, and in one case with the host name as the value. Resolvers returned multiple answers ("multiple DNS replies") and DKIM failed. One clean TXT record per selector fixed it.
  2. Comments in Postfix virtual files. A trailing # comment on an entry line is not a comment there. The words became extra recipients. Comments go on their own line.
  3. Spamhaus and public resolvers. Spamhaus refuses queries from public DNS resolvers and returns 127.255.255.x codes. Naive blocklist checkers read that as "listed". It isn't; query through your own resolver.
  4. UCEPROTECT level 3. It lists whole provider networks, OVH included. That isn't a listing of your IP.
  5. Port 25. AWS and some other clouds block outbound port 25 by default. OVH didn't in our case. Check your provider before you build.

Would we do it again?

Yes, for our situation: several domains, a server we already ran, and people comfortable owning Postfix. If you send small volumes and nobody wants to look after a server, a hosted API is still a reasonable choice.

If you'd like the same migration done for you, with fallback and warm-up included, see our email migration service or the Server Setup and Managed Server plans.