Why a Frugal Solo Developer Might (Actually) Want to Run Their Own Email Server
November 01, 2025
Postfix + Dovecot + OpenDKIM on Debian/Ubuntu, with a focus on transactional mail, and a frank look at the potholes you’ll hit along the way.
If you’re a solo developer working on a side project, a micro-SaaS, or a hobby app, you’ve probably felt the pinch: most transactional email providers have axed their generous free tiers. That welcome email, password reset, or invoice notification now costs real money—and, over time, that can add up. Meanwhile, your project’s email throughput is tiny, your margins are thinner than your patience, and your users expect messages to “just work.”
So, should you run your own email server?
This article is a practical, sober-minded guide to self-hosting email for transactional use. We’ll cover why you might choose to do it (beyond philosophical satisfaction), what stack to use (Postfix, Dovecot, OpenDKIM on Debian/Ubuntu), and the biggest pitfalls you’ll encounter (deliverability is a four-letter word). You’ll get concrete setup notes, DNS examples, operational checklists, and mitigations that keep you on the good side of Gmail and friends. The tone is honest: this is absolutely doable for a technically inclined developer—but it’s not a “set it and forget it” toaster.
Who is this for?: frugal, solo developers who send modest volumes of transactional mail and prefer control over convenience.
acknowledging my bias: sympathetic to self-hosting, but allergic to delusion.
TL;DR (so you can decide whether to bail now)
- Running your own mail server can save money at low volumes and gives you full control over privacy, policy, and integration.
- It’s also a responsibility: deliverability is hard, IP reputation is fickle, and misconfigurations are common.
- A realistic approach: start with outbound transactional mail, add inbound later; harden with SPF/DKIM/DMARC; monitor bounces; and consider a hybrid fallback SMTP relay.
- If you enjoy learning infrastructure and accept some ongoing maintenance, it’s rewarding. If you want zero ops, it’s not.
Why a Solo Developer Would Self-Host Email
Let’s get beyond “because free tiers are gone”—though that’s reason enough for many.
-
Cost control for low volume If you send a few hundred transactional emails a month, paying a baseline subscription to an ESP (Email Service Provider) can feel like burning money. A small VPS (with a static IP and decent reputation) plus your time can be cheaper in the long run. You’ll pay for the server, the domain, and your effort—not per-message markups.
-
Independence from shifting provider policies Third-party email providers can change pricing, rate limits, acceptable-use policies, or even suspend you for ambiguous “abuse” signals. Owning the stack removes a single point of vendor failure. Your domain, your keys, your queue.
-
Privacy and data locality Transactional emails contain sensitive data: password reset links, invoices, 2FA codes. Keeping mail on your infrastructure—encrypted at rest with auditable access—reduces exposure. You also avoid vendor analytics creeping into your messages.
-
First-class integration and debugging With Postfix/Dovecot logs at your fingertips, you can trace SMTP dialog, bounce codes, greylisting behavior, and latency end-to-end. You’re not a support ticket number waiting on an upstream provider—you are the upstream.
-
Predictable branding and headers Full control over headers, DKIM selectors, ARC/DMARC alignment, and return-paths means better authentication and “cleaner” messages that reflect your brand. Your envelope sender, not
@some-relay.example. -
Educational value Running email is a masterclass in real-world networking: DNS, TLS, authentication, abuse handling, queue management, and performance under constraints. If you ever plan to scale infrastructure, the lessons transfer.
-
Narrow, transactional scope is tractable We’re not talking about hosting a 500-seat corporate inbox system. Limiting scope to outbound transactional email (plus a catch-all support inbox) keeps complexity reasonable.
-
A graceful escape hatch exists You can always bolt on a low-cost relay (e.g., an SMTP smarthost) for tough destinations or bursts—even temporarily—without rewriting app code.
The Stack: Postfix, Dovecot, OpenDKIM on Debian/Ubuntu
- Postfix: the MTA (Mail Transfer Agent) that sends and receives mail, handles queues, applies policies, and talks SMTP.
- Dovecot: the IMAP/POP3 server your clients (or your helpdesk app) use to read mail, and the SASL provider for authenticated submission.
- OpenDKIM: signs outgoing mail with DKIM so receivers can verify authenticity.
-
Optional but recommended:
- rspamd or SpamAssassin for spam filtering (especially if you accept inbound).
- OpenDMARC for DMARC policy reporting/analytics.
- Certbot for TLS certificates.
- fail2ban and UFW for basic hardening.
OS: Debian or Ubuntu LTS. They’re stable, well-documented, and the package maintainers care about sane defaults.
Architecture at 10,000 Feet
- Your domain:
example.dev - One VPS with a static, clean IP (ideally not long-abused by spammers from the provider’s address space).
-
Postfix listens on:
- 25 (SMTP) for inbound from the world.
- 587 (submission) for authenticated outbound from your apps and devices.
- 465 (smtps, optional) for legacy clients that insist on implicit TLS.
-
Dovecot listens on:
- 143/993 (IMAP/IMAPS) for reading mail.
- OpenDKIM signs messages originating from your domain on the way out via Postfix’s milter interface.
- DNS: correct A, AAAA (if using IPv6), MX, SPF, DKIM, DMARC, PTR (reverse DNS), and optionally MTA-STS and TLS-RPT.
Pre-Flight Checklist
- A domain you control (
example.dev) with access to DNS. -
A VPS with:
- Static IPv4 address (and IPv6 if you can keep it clean).
- Ports 25, 587, 465, 143, 993 open and not blocked by the provider. (Some low-cost providers block 25 by default; ask for an unblock or plan a relay.)
- A proper PTR record you can set or request (
mail.example.dev).
- Time to keep the system patched, monitored, and backed up.
- Realistic expectations: Gmail and Microsoft are strict. You’ll earn deliverability gradually.
Installing the Stack (Ubuntu/Debian)
Note: The configs below are intentionally minimal yet production-oriented. Adapt paths to your environment.
1) System Prep
sudo apt update && sudo apt upgrade -y
sudo apt install -y postfix postfix-pcre postfix-policyd-spf-python \
dovecot-imapd dovecot-lmtpd dovecot-sieve dovecot-managesieved \
opendkim opendkim-tools certbot python3-certbot-nginx \
ufw fail2ban
Set your server’s hostname:
sudo hostnamectl set-hostname mail.example.dev
echo "127.0.1.1 mail.example.dev mail" | sudo tee -a /etc/hosts
2) TLS Certificate (Let’s Encrypt)
If you run Nginx or Apache, you can use HTTP-01; otherwise use standalone:
sudo systemctl stop postfix dovecot
sudo certbot certonly --standalone -d mail.example.dev
sudo systemctl start postfix dovecot
Certificates land under /etc/letsencrypt/live/mail.example.dev/. Automate renewals (certbot installs a timer).
3) Postfix: Core Configuration
/etc/postfix/main.cf:
# Identity
myhostname = mail.example.dev
myorigin = /etc/mailname
mydestination = localhost
mydomain = example.dev
# Network & protocols
inet_interfaces = all
inet_protocols = ipv4
mynetworks_style = host
# Mailbox format
home_mailbox = Maildir/
# TLS
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.dev/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.dev/privkey.pem
smtpd_tls_security_level = may
smtp_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_use_tls = yes
smtp_use_tls = yes
smtpd_tls_loglevel = 1
# SASL auth (via Dovecot)
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
# Authenticated submission restrictions
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination
# HELO/EHLO checks
smtpd_helo_required = yes
# DKIM via milter
milter_default_action = accept
milter_protocol = 2
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
# SPF check (optional lightweight)
policyd-spf_time_limit = 3600s
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_destination,
check_policy_service unix:private/policyd-spf
# Rate limiting (basic)
smtpd_client_connection_rate_limit = 30
smtpd_client_message_rate_limit = 100
# Queue settings (tunable)
maximal_queue_lifetime = 3d
bounce_queue_lifetime = 1d
/etc/postfix/master.cf (enable submission ports):
smtp inet n - y - - smtpd
submission inet n - y - - smtpd
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
smtps inet n - y - - smtpd
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
policyd-spf unix - n n - - spawn
user=policyd-spf argv=/usr/bin/policyd-spf
Reload:
sudo systemctl enable postfix
sudo systemctl restart postfix
4) Dovecot: IMAP & Auth
/etc/dovecot/dovecot.conf:
protocols = imap lmtp
listen = *
ssl = required
ssl_cert = </etc/letsencrypt/live/mail.example.dev/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.example.dev/privkey.pem
auth_mechanisms = plain login
!include conf.d/*.conf
/etc/dovecot/conf.d/10-mail.conf:
mail_location = maildir:~/Maildir
mail_privileged_group = mail
/etc/dovecot/conf.d/10-auth.conf:
disable_plaintext_auth = yes
auth_mechanisms = plain login
!include auth-system.conf.ext
/etc/dovecot/conf.d/10-master.conf (SASL socket for Postfix):
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
/etc/dovecot/conf.d/20-lmtp.conf:
protocol lmtp {
postmaster_address = postmaster@example.dev
}
Reload:
sudo systemctl enable dovecot
sudo systemctl restart dovecot
Create a mail user (system user) for testing:
sudo adduser alice
# Set a strong password; this user will own /home/alice/Maildir
5) OpenDKIM: Signing Outbound Mail
Generate a DKIM key:
sudo mkdir -p /etc/opendkim/keys/example.dev
cd /etc/opendkim/keys/example.dev
sudo opendkim-genkey -s default -d example.dev
sudo chown opendkim:opendkim default.private
/etc/opendkim.conf:
Syslog yes
UMask 002
Domain example.dev
KeyFile /etc/opendkim/keys/example.dev/default.private
Selector default
Socket inet:8891@localhost
Canonicalization relaxed/simple
Mode s
OversignHeaders From
AutoRestart Yes
AutoRestartRate 10/1h
TrustAnchorFile /usr/share/dns/root.key
# For multiple domains, use KeyTable/SigningTable; for one, the above is fine.
Alternatively, for multi-domain setups, define:
/etc/opendkim/KeyTable:
default._domainkey.example.dev example.dev:default:/etc/opendkim/keys/example.dev/default.private
/etc/opendkim/SigningTable:
*@example.dev default._domainkey.example.dev
/etc/opendkim/TrustedHosts:
127.0.0.1
::1
localhost
mail.example.dev
Enable and start:
sudo systemctl enable opendkim
sudo systemctl restart opendkim
sudo systemctl restart postfix
Publish the DKIM record (default._domainkey.example.dev) using the contents of default.txt.
6) Firewall & Basic Hardening
sudo ufw allow 22/tcp
sudo ufw allow 25/tcp
sudo ufw allow 587/tcp
sudo ufw allow 465/tcp
sudo ufw allow 143/tcp
sudo ufw allow 993/tcp
sudo ufw enable
Install fail2ban jails for Postfix and Dovecot to block brute force attempts. Keep the OS patched (unattended-upgrades helps).
The DNS You Must Get Right (or Nothing Works)
-
A/AAAA:
mail.example.dev -> your server IP(s) -
MX:
example.dev -> mail.example.dev (priority 10) -
SPF (TXT):
example.dev. IN TXT "v=spf1 mx ~all"If you relay through another host, include it.
-
DKIM (TXT):
default._domainkey.example.dev. IN TXT "v=DKIM1; k=rsa; p=MIIBI... (your key) ..." -
DMARC (TXT):
_dmarc.example.dev. IN TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; ruf=mailto:[email protected]; fo=1; adkim=s; aspf=s"Start with
p=noneto monitor; tighten toquarantineorrejectas you gain confidence. -
PTR (Reverse DNS): Your IP →
mail.example.dev(ask your VPS provider to set this). Non-negotiable for deliverability. -
Optional: MTA-STS & TLS-RPT:
_mta-sts.example.dev. IN TXT "v=STSv1; id=20250101"-
mta-sts.example.devhosts a policy file at/.well-known/mta-sts.txt:version: STSv1 mode: enforce mx: mail.example.dev max_age: 604800 _smtp._tls.example.dev. IN TXT "v=TLSRPTv1; rua=mailto:[email protected]"
These last two improve transport security and yield useful reports.
Transactional Email: Patterns and Plumbing
Running your own server is not just “send mail and pray.” Transactional flows require predictable behavior and observability.
-
Submission from your app(s) Configure your application to authenticate to
mail.example.devon 587 with STARTTLS, using a dedicated credential (not a human user). Keep per-app identities to rotate credentials without impact. -
Envelope sender and Return-Path Use a consistent envelope sender like
[email protected]with a controlled mailbox or alias for bounces. This must align with DMARC policy (domain alignment matters). -
VERP or unique return paths For high-signal bounce handling, vary the return path per recipient (VERP) or include a token so you can match bounces to recipients/events. Postfix supports variable expansion in
sender_bcc_mapsor you can generate per-message return paths in your app. -
Idempotence and retries Transactional events (password resets, receipts) should be idempotent or at least safe to resend. Postfix will retry temporarily failed messages; ensure your app logic won’t double-charge or double-activate.
-
Bounce processing Parse DSNs (Delivery Status Notifications) to adjust future sends (e.g., mark addresses as invalid). Even low volume benefits from a simple bounce processor.
-
Templates and headers Keep messages lean: proper
From:,To:,Date:,Message-ID:, andList-*headers (even if not a list) can help filters. Include a plain-text alternative for HTML emails. -
Rate limiting Throttle sends to cautious domains (Gmail, Outlook) if you see temp failures. It’s better to deliver a little later than to get rate-limited into oblivion.
The Common Pitfalls (and How to Dodge Them)
Email’s reputation for being “hard” is earned. Here are the most frequent head-smackers.
1) Port 25 Is Blocked
Symptom: you can send via submission (587) to your own server, but no one on the internet gets mail from you. Fix: choose a VPS that unblocks port 25 for outbound SMTP, or open a support ticket to request unblocking. If that’s impossible, configure Postfix to relay outbound mail through a smarthost (another provider), at least initially.
2) No Reverse DNS (PTR)
Symptom: large providers silently file you in spam or hard-reject with “PTR invalid.”
Fix: set the PTR to your mail hostname (mail.example.dev) and ensure forward and reverse match. Ask your VPS provider if you can’t set it yourself.
3) Weak or Misaligned SPF/DKIM/DMARC
Symptom: DMARC fails because From: is @example.dev but your envelope sender is @mailer.otherdomain.
Fix: align your domains. Sign with DKIM for your From domain; ensure SPF authorizes your sending host; set DMARC to monitor first (p=none) and tighten later.
4) Sending from a “dirty” IP range
Symptom: instant spam folder despite perfect DNS. Fix: pick a provider with decent IP reputation, or use a relay for initial warm-up. Avoid rapidly rotating IPs; consistency breeds reputation.
5) Over-eager content or tracking
Symptom: transactional email looks like a newsletter (lots of images, trackers, shortened URLs). Fix: keep transactional mail plain and functional. Avoid link shorteners. Use your own domain for links.
6) TLS issues
Symptom: receivers downgrade or complain about weak ciphers; some reject. Fix: maintain Let’s Encrypt certs, prefer modern ciphers, and enforce STARTTLS on submission. Renewals must be automated.
7) Open relay (the worst timeline)
Symptom: your queue explodes; your IP gets blacklisted.
Fix: never permit anonymous relay. Only permit_mynetworks (your box) and permit_sasl_authenticated. Carefully audit smtpd_recipient_restrictions.
8) Ignoring bounces and feedback loops
Symptom: you keep sending to bad addresses, which hammers your reputation. Fix: parse bounces, suppress invalid addresses, and respect unsubscribes (even for “transactional” where appropriate).
9) Greylisting & throttling misinterpretation
Symptom: transient 4xx errors make you think you’re blocked. Fix: Postfix will retry; don’t panic. Watch for persistent patterns, not single events.
10) Logging, metrics, and alerting after the fact
Symptom: you discover problems days later. Fix: tail logs, ship them to a viewer, or expose metrics. Postfix has exporters for Prometheus; even simple logwatch alerts help.
11) No backups
Symptom: a disk failure becomes a data loss event.
Fix: back up /etc/ configs, DKIM keys, and Maildir. Use off-site encrypted backups.
12) Accepting inbound spam without filters
Symptom: your users’ inboxes fill with junk; your server’s resources get chewed.
Fix: add rspamd or SpamAssassin + postscreen. Consider greylisting inbound; it’s effective against bot spam.
Deliverability: Earning Trust Without a Brand Name Behind You
Deliverability boils down to: authentication, reputation, content, and consistency.
-
Authentication SPF, DKIM, and DMARC are table stakes. Verify alignment. Use strict
adkim=sandaspf=sonce stable. Publish a DMARC record that reports (rua,ruf) so you can see who’s pretending to be you. -
Reputation Send from your own domain and IP consistently. Start with low volume, grow slowly. Avoid spikes. If you inherited a questionable IP, warm up by sending to your own test accounts at major providers and to consenting recipients who will open and engage.
-
Content Transactional email should be minimal, readable, and accessible. Provide a text part. Avoid attachments unless necessary. Host images on your domain with HTTPS. Avoid spammy phrasing and excessive links.
-
Consistency Keep “From” names steady. Use the same
Message-IDdomain. Send resets immediately; send receipts once; avoid duplicates. The less surprise, the better. -
Monitoring
- DMARC reports (parse them; there are open-source tools).
- Postfix logs (
/var/log/mail.log) for 4xx/5xx patterns. - Occasional checks with tools that show how receivers see your SPF/DKIM/DMARC.
- Watch public blocklists; many have web checkers.
Reality check: Gmail and Microsoft sometimes require more than “standards compliance.” If you run into a brick wall, consider relaying just those domains through a third-party until your reputation matures.
Operating Playbook (So You Don’t Become Your Own On-Call Nightmare)
-
Daily (or automated):
- Check mail logs for unusual spikes, 4xx/5xx clusters, or queue backlogs (
postqueue -p). - Ensure certificates are valid (
certbot renew --dry-runvia cron or timer is fine).
- Check mail logs for unusual spikes, 4xx/5xx clusters, or queue backlogs (
-
Weekly:
- Apply OS updates.
- Review DMARC/TLS-RPT reports if configured.
- Verify backups completed and are restorable.
-
Monthly:
- Rotate DKIM selectors annually (or sooner if you like hygiene).
- Reassess DMARC policy (move from
none→quarantine→rejectonce confident). - Audit fail2ban bans and whitelist legitimate sources if needed.
-
On incident:
- Backlog in queue?
postqueue -p, look at common status codes. - Blacklisted? Identify the list, follow their delisting procedure; fix root cause first.
- Credential leak? Rotate submission passwords, invalidate tokens, audit logs.
- Backlog in queue?
Cost Model: Does This Actually Save Money?
- VPS: $5–$10/month for a modest instance with unblocked SMTP and static IP.
- Domain: $10–$20/year.
- Your time: the wildcard. Initial setup might take a few focused hours; ongoing ops is minimal if you keep things clean.
- Hidden costs: a blocked IP that needs a paid relay; time spent on delisting; a brief deliverability slump.
If you send very low volume (hundreds/month), this is competitive with paid tiers. If you scale to tens of thousands/month, an ESP’s economy of scale, analytics, and deliverability teams may justify their cost. Many developers find a middle ground: self-host for learning and privacy, and use a backup relay for tough destinations or spikes.
Security & Privacy: What You Gain (and What You Must Uphold)
Gains:
- Data locality: your messages rest on disks you control.
- Minimal data exhaust: no vendor-level tracking or analytics injected into your messages.
- Auditability: full logs; with the responsibility to safeguard them.
Responsibilities:
- Encryption at rest: use full-disk encryption (e.g., LUKS) and store keys securely.
- Access control: least privilege; separate service accounts for your app’s submission.
- Patch discipline: treat this like any internet-facing service—because it is.
- Legal compliance: even transactional mail must respect local laws (e.g., accurate sender info, data protection obligations).
- Incident response: have a plan for credential leaks or abuse.
A Minimal, Workable Setup: Putting It Together
- Stand up Ubuntu LTS on a VPS with an unblocked 25/tcp and set
hostnametomail.example.dev. - Issue a Let’s Encrypt certificate.
- Install Postfix, Dovecot, OpenDKIM; configure submission on 587 with STARTTLS and SASL via Dovecot.
- Publish correct DNS: A/AAAA, MX, SPF, DKIM, DMARC, PTR.
- Create an app-specific user and send test mail using authenticated submission.
- Verify authentication with a test account at Gmail/Outlook/Yahoo; inspect headers for
spf=pass,dkim=pass,dmarc=pass. -
Keep logs open while testing:
sudo tail -f /var/log/mail.log - Add bounce processing in your app (even a simple parser).
- Monitor; iterate; tighten DMARC when satisfied.
Example: App Configuration (Node.js SMTP Submission)
A concise nod to the app side—use 587 and STARTTLS:
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: 'mail.example.dev',
port: 587,
secure: false, // STARTTLS
auth: {
user: 'appmailer', // dedicated submission user
pass: process.env.MAIL_PASS
},
tls: {
rejectUnauthorized: true
}
});
await transporter.sendMail({
from: 'Example <[email protected]>',
to: '[email protected]',
subject: 'Your password reset',
text: 'Here is your reset link: https://example.dev/reset/abc',
html: 'Here is your reset link: <a href="https://example.dev/reset/abc">Reset</a>',
headers: {
'X-Transaction-Type': 'password-reset'
}
});
Create appmailer as a system user or—better—use Dovecot’s passdb/userdb for virtual users if you don’t want shell accounts.
Troubleshooting: Quick Recipes
-
Check open ports:
ss -lntp | egrep ':25|:587|:465|:143|:993' -
Send a test from the server:
swaks --to [email protected] --server mail.example.dev --auth LOGIN \ --auth-user appmailer --auth-password '...' -
Inspect received headers (in Gmail, show original): look for
spf=pass,dkim=pass,dmarc=pass. -
Watch queue:
postqueue -p -
Force retry a message:
postsuper -r <queue-id> -
Flush queue:
postfix flush -
DKIM test:
opendkim-testkey -d example.dev -s default -vvv
When Not to Self-Host (Yes, Sometimes It’s a Bad Idea)
- You need high-volume or marketing features: templates, A/B testing, rich analytics—use an ESP.
- You can’t get an unblocked IP: fighting your infrastructure is not frugal.
- You don’t have time for stewardship: if patching, monitoring, and occasional debugging sound dreadful, don’t do it.
- You need strict deliverability SLAs: paid providers invest in relationships and tooling you won’t replicate alone.
Hybrid Patterns (Pragmatism Without Shame)
- Self-host inbound, relay outbound: run Dovecot for inboxes; send via a low-cost relay to demanding domains.
- Selective relay by domain: deliver direct except for
gmail.comandoutlook.com, which route via smarthost. - Failover relay: attempt direct delivery; on temporary failures after N retries, switch to relay.
Postfix supports transport maps to route by domain:
/etc/postfix/transport:
gmail.com smtp:[smtp-relay.somprovider.com]:587
outlook.com smtp:[smtp-relay.somprovider.com]:587
Then:
postmap /etc/postfix/transport
And in main.cf:
transport_maps = hash:/etc/postfix/transport
A Note on Mail Storage: Maildir vs. Virtual Users
For one or two inboxes, system users + Maildir is fine. If you want multiple aliases or domains without shell accounts, move to virtual users:
- Postfix virtual alias maps.
- Dovecot
passdb/userdbwith a dedicated directory (e.g.,/var/mail/vhosts/example.dev/user/). - Authentication via Dovecot against a file, SQL, or LDAP (overkill for solo devs, but good to know).
Keep it simple until it isn’t.
Expanding the Security Surface (Optional but Sensible)
- Sieve rules via Dovecot for server-side filtering (e.g., file receipts into a
Receipts/folder). - postscreen to pre-screen abusive hosts at SMTP connect time.
- rspamd for spam classification with modern heuristics (outperforms classic SpamAssassin for many).
- Two-factor on your system accounts (SSH), and consider TOTP-backed IMAP logins (via app passwords) if you must allow client access from multiple devices.
- Audit logs to a separate storage or logging service.
Realistic Expectations and a Path to Success
- Week 1: Set up server; send test transactional emails to yourself; fix SPF/DKIM/DMARC; ensure bounces work.
- Week 2–3: Route your app’s low-stakes transactional messages (sign-ups, confirmations). Monitor deliverability.
- Month 2: Tighten DMARC to
quarantine. Add inbound support mailbox if needed; integrate IMAP with your helpdesk or client. - Month 3: Evaluate whether you need selective relay for specific domains. If all is well, consider
p=reject.
This staged approach balances learning, risk, and reliability.
Frequently Asked (Pragmatic) Questions
Q: Will Gmail put me in spam? A: Early on, maybe. With correct auth, sane content, and consistent sending, you can land in inboxes. If you struggle, selectively relay Gmail while warming up.
Q: Is IPv6 required? A: No, but it’s nice to have. If you enable it, ensure reverse DNS for IPv6 too—or you might hurt deliverability.
Q: Can I track opens and clicks? A: You can, but privacy-preserving transactional mail should be conservative. If you must, host tracking under your domain and be transparent.
Q: What about ARC, BIMI, and newer standards? A: ARC helps when forwarding; BIMI requires logos and strict DMARC. They’re nice add-ons, not prerequisites for transactional deliverability.
Closing Thoughts
Running your own email server in 2025 sounds contrarian—like building furniture instead of buying it flat-packed. But if you’re a frugal solo developer sending low volumes of transactional mail, the math and the autonomy can make sense. You control costs, policies, privacy, and debugging. You also accept the burden of care: DNS diligence, TLS hygiene, bounce handling, and reputation management.
My recommendation is measured:
- Start small with outbound transactional only.
- Keep the configuration minimal and standards-compliant.
- Monitor early and often.
- Don’t hesitate to use a hybrid relay when reality demands it.
Done right, self-hosting email becomes a stable, boring piece of your stack—exactly what you want for something as unglamorous and as essential as “send the password reset.”
Appendix: Copy-Pasteable Snippets
SPF (TXT):
v=spf1 mx ~all
DMARC (TXT, monitor first):
v=DMARC1; p=none; rua=mailto:dmarc-reports@example.dev; ruf=mailto:dmarc-forensics@example.dev; fo=1; adkim=s; aspf=s
Postfix log watch (systemd journal):
journalctl -u postfix -f
Dovecot log watch:
journalctl -u dovecot -f
Queue management:
postqueue -p
postsuper -d ALL deferred
Renew certs (non-webserver):
certbot renew --pre-hook "systemctl stop postfix dovecot" \
--post-hook "systemctl start postfix dovecot"
Fail2ban quick install for Postfix & Dovecot (jail snippet):
[postfix]
enabled = true
port = smtp,ssmtp,submission,imap,imaps
filter = postfix
logpath = /var/log/mail.log
[dovecot]
enabled = true
port = pop3,pop3s,imap,imaps,submission,465,587
filter = dovecot
logpath = /var/log/mail.log