Running your own mail server is only useful if the messages land in the inbox. Gmail, Outlook and Yahoo decide that based on who you are (reverse DNS and IP reputation), whether you can prove the mail is yours (SPF, DKIM and DMARC) and how you behave (volume, bounces and complaints). In this tutorial you will fix all three on an Ubuntu 24.04 server running Postfix: set a matching PTR and HELO name, publish SPF, sign outgoing mail with OpenDKIM, roll out DMARC, check your IP against the main blocklists and warm up a new IP safely.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS with a working Postfix installation that already sends mail, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • A domain you control, referred to as your_domain, and access to its DNS zone.
  • A mail hostname such as mail.your_domain with an A record pointing to the server's public IP, referred to as your_server_ip.
  • Outbound TCP port 25 allowed by your provider. Many providers block it by default on new accounts, so confirm this before troubleshooting anything else.
  • An external mailbox for testing, such as a Gmail account.

Step 1 - Aligning reverse DNS and the HELO name

Receiving servers look up the PTR record of the connecting IP, then check that the name resolves back to the same IP (forward-confirmed reverse DNS). Gmail rejects or spam-folders mail from IPs without a valid PTR, so this is the first thing to get right.

Check the current PTR record of your IP:

dig +short -x your_server_ip

The answer must be your mail hostname, with a trailing dot:

mail.your_domain.

Then confirm that the name resolves back to the same IP:

dig +short A mail.your_domain
your_server_ip

If the PTR is missing or shows a generic provider name, set it to mail.your_domain through your hosting provider. The PTR belongs to whoever owns the IP block, not to your domain's DNS zone.

Next, make Postfix announce the same name in its EHLO greeting. Set myhostname, which Postfix also uses as the default smtp_helo_name:

sudo postconf -e 'myhostname = mail.your_domain'
sudo systemctl reload postfix

Verify the value:

postconf myhostname smtp_helo_name
myhostname = mail.your_domain
smtp_helo_name = $myhostname

While you are here, confirm that Postfix is not an open relay. The Ubuntu default restricts relaying to local networks and authenticated users:

postconf smtpd_relay_restrictions
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination

If you see permit without reject_unauth_destination or defer_unauth_destination, fix it before sending anything: an open relay will be abused and blocklisted within hours.

Step 2 - Publishing an SPF record

SPF lists the servers allowed to send mail for your domain. Receivers compare the IP of the connecting server with this list.

Create a TXT record on the root of your_domain in your DNS provider. If this server is also your MX, the simplest correct record is:

v=spf1 mx a:mail.your_domain -all

If you also send through a third-party service, add its include: mechanism before -all, for example include:_spf.google.com. Keep a single SPF record per name: two v=spf1 TXT records on the same name make SPF fail with a permanent error. Use ~all (soft fail) while you are still discovering all your senders, and switch to -all once DMARC reports show nothing legitimate failing.

Check the published record:

dig +short TXT your_domain
"v=spf1 mx a:mail.your_domain -all"

Step 3 - Signing outgoing mail with OpenDKIM

DKIM adds a cryptographic signature to each message. The receiver fetches your public key from DNS and verifies that the message was not altered and really comes from your domain. It is the mechanism that survives forwarding, which SPF does not.

Install OpenDKIM and its tools:

sudo apt update
sudo apt install opendkim opendkim-tools

Generate a 2048-bit key pair with the selector mail. The selector is just a label that lets you rotate keys later:

sudo mkdir -p /etc/opendkim/keys/your_domain
sudo opendkim-genkey -b 2048 -d your_domain -D /etc/opendkim/keys/your_domain -s mail -v
sudo chown -R opendkim:opendkim /etc/opendkim/keys
sudo chmod 600 /etc/opendkim/keys/your_domain/mail.private

This creates mail.private (the private key) and mail.txt (the DNS record). Print the record:

sudo cat /etc/opendkim/keys/your_domain/mail.txt
mail._domainkey	IN	TXT	( "v=DKIM1; h=sha256; k=rsa; "
	  "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv3..."
	  "...IDAQAB" )  ; ----- DKIM key mail for your_domain

Create a TXT record named mail._domainkey.your_domain and paste the value as one string, joining the quoted parts without the quotes and without spaces between them: v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBg...IDAQAB. Most DNS panels split long values automatically.

Now configure OpenDKIM. Open the main configuration file:

sudo nano /etc/opendkim.conf

Find the existing Socket local:/run/opendkim/opendkim.sock line and comment it out, because Postfix on Ubuntu runs chrooted and cannot reach that socket. Then add the following lines at the end of the file:

Socket                  inet:8891@localhost
Domain                  your_domain
Selector                mail
KeyFile                 /etc/opendkim/keys/your_domain/mail.private
Mode                    sv

Mode sv makes OpenDKIM sign outgoing mail and verify incoming mail. By default it signs messages from 127.0.0.1 and from clients that authenticated with SASL, which covers both local scripts and your users sending through port 587.

Restart OpenDKIM and check that it listens on port 8891:

sudo systemctl restart opendkim
sudo ss -tlnp | grep 8891
LISTEN 0      4096       127.0.0.1:8891       0.0.0.0:*    users:(("opendkim",pid=4121,fd=3))

Connect Postfix to the milter. If you already use other milters (OpenDMARC, rspamd), append inet:localhost:8891 to your existing list instead of replacing it:

sudo postconf -e 'milter_default_action = accept'
sudo postconf -e 'milter_protocol = 6'
sudo postconf -e 'smtpd_milters = inet:localhost:8891'
sudo postconf -e 'non_smtpd_milters = $smtpd_milters'
sudo systemctl reload postfix

milter_default_action = accept keeps mail flowing if OpenDKIM is down, at the cost of sending that mail unsigned.

Once the DNS record has propagated, test the key:

sudo opendkim-testkey -d your_domain -s mail -vvv
opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: checking key 'mail._domainkey.your_domain'
opendkim-testkey: key not secure
opendkim-testkey: key OK

key not secure only means your zone is not signed with DNSSEC. key OK is the line that matters.

Step 4 - Rolling out DMARC

DMARC tells receivers what to do when a message fails both SPF and DKIM alignment, and asks them to send you daily aggregate reports. Alignment means the domain in the visible From: header matches the domain that passed SPF (the envelope sender) or DKIM (the d= tag).

Start in monitoring mode. Create a TXT record named _dmarc.your_domain:

v=DMARC1; p=none; rua=mailto:dmarc@your_domain

Make sure the dmarc@your_domain mailbox exists. Reports arrive as gzip or zip XML files from each large provider, usually once a day. Read them for two to four weeks to find every legitimate source sending as your domain (newsletters, invoicing tools, CRM) and fix their SPF and DKIM.

When the reports show only your own, passing sources, tighten the policy in stages:

v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc@your_domain
v=DMARC1; p=reject; rua=mailto:dmarc@your_domain

Verify the record:

dig +short TXT _dmarc.your_domain
"v=DMARC1; p=none; rua=mailto:dmarc@your_domain"

Since 2024 Gmail and Yahoo require any sender of more than 5,000 messages a day to their users to have SPF, DKIM and a DMARC record (at least p=none), with the From: domain aligned. Outlook.com applies the same rule. Smaller senders still need SPF or DKIM plus a valid PTR.

Step 5 - Verifying authentication with a real message

Send a test message through Postfix to your external mailbox. Postfix provides a sendmail command, so no extra mail client is needed:

printf 'From: you@your_domain\nTo: [email protected]\nSubject: Deliverability test\n\nThis is a test.\n' | sudo sendmail -t -f you@your_domain

Check that Postfix handed it off to Gmail:

sudo grep 'status=sent' /var/log/mail.log | tail -n 1
2026-09-25T10:14:02.118+00:00 mail postfix/smtp[5123]: 7A1B2C3D4E: to=<[email protected]>, relay=gmail-smtp-in.l.google.com[142.250.x.x]:25, delay=1.2, dsn=2.0.0, status=sent (250 2.0.0 OK)

In Gmail, open the message and choose Show original. The summary at the top must show:

SPF:   PASS with IP your_server_ip
DKIM:  'PASS' with domain your_domain
DMARC: 'PASS'

For a second opinion that also checks content and blocklists, send a message to the address shown on mail-tester.com and aim for 9/10 or better.

Step 6 - Checking your IP against blocklists

A DNS-based blocklist (DNSBL) is queried by reversing the octets of the IP and appending the list's zone. For 203.0.113.10 you query 10.113.0.203.zen.spamhaus.org. An empty answer means not listed; an answer in 127.0.0.x means listed.

dig +short 10.113.0.203.zen.spamhaus.org

Spamhaus refuses queries coming through large public resolvers (Google, Cloudflare) and answers 127.255.255.254 instead, which is not a listing. If you see that, query through your server's own resolver or use the Spamhaus lookup page.

To check this regularly, create a small script. Open a new file:

sudo nano /usr/local/bin/check-dnsbl

Add the following content:

#!/usr/bin/env bash
# Print a line for every DNSBL that lists the given IPv4 address.
# Prints nothing when the IP is clean, so cron only mails on a listing.
set -euo pipefail

ip="${1:?usage: check-dnsbl IPv4_ADDRESS}"
IFS=. read -r a b c d <<< "$ip"
reversed="${d}.${c}.${b}.${a}"

for zone in zen.spamhaus.org bl.spamcop.net b.barracudacentral.org; do
    answer="$(dig +short +time=3 +tries=2 "${reversed}.${zone}" A || true)"
    if [[ "$answer" == 127.0.0.* ]]; then
        echo "${ip} is listed on ${zone}: ${answer//$'\n'/ }"
    fi
done

Make it executable and run it once:

sudo chmod 755 /usr/local/bin/check-dnsbl
/usr/local/bin/check-dnsbl your_server_ip

No output means the IP is clean on all three lists. Schedule a daily run with cron, which mails any output to the address in MAILTO through your own Postfix:

sudo nano /etc/cron.d/check-dnsbl
MAILTO=admin@your_domain
15 6 * * * root /usr/local/bin/check-dnsbl your_server_ip

If you do get listed, stop the cause first (a compromised account, a form sending spam, a bad list), then request removal through the list's own lookup page. Spamhaus and Barracuda offer delisting forms; SpamCop entries expire on their own about 24 hours after the reports stop.

For reputation data straight from the receivers, register your domain in Google Postmaster Tools and your IP in Microsoft SNDS. Both show spam complaint rates that no blocklist will tell you about. Gmail expects the user-reported spam rate to stay below 0.3%, and ideally below 0.1%.

Step 7 - Warming up a new IP and throttling Postfix

A new IP has no history, and a sudden burst of thousands of messages from it looks like spam. Increase volume gradually and send first to the recipients most likely to open your mail:

PeriodMessages per day
Days 1-350-100
Days 4-7200-500
Week 21,000-2,000
Week 35,000-10,000
Week 4 onwardsRoughly double each week while complaints stay low

Keep sending every day: long gaps slow down the warm-up. Pause and investigate if the bounce rate goes above 2% or complaints above 0.1%.

Postfix can also pace delivery per destination so a single provider never sees a burst. Add these settings:

sudo postconf -e 'smtp_destination_concurrency_limit = 5'
sudo postconf -e 'smtp_destination_rate_delay = 1s'
sudo systemctl reload postfix

smtp_destination_concurrency_limit caps parallel connections per destination domain, and smtp_destination_rate_delay waits the given time between deliveries to the same destination (a non-zero value also limits concurrency to one per destination). Remove the delay once your volume is stable.

If you send newsletters or other bulk mail, add a one-click unsubscribe to every message, as Gmail and Yahoo require for bulk senders:

List-Unsubscribe: <https://your_domain/unsubscribe?id=RECIPIENT_TOKEN>, <mailto:unsubscribe@your_domain>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

Step 8 - Handling bounces

Every message sent to a non-existent address hurts your reputation, so remove addresses that hard-bounce. List recent bounces with their reason:

sudo grep 'status=bounced' /var/log/mail.log | tail -n 20
... postfix/smtp[6021]: 9C8D7E6F5A: to=<[email protected]>, relay=mx.example.com[198.51.100.7]:25, delay=0.8, dsn=5.1.1, status=bounced (host mx.example.com said: 550 5.1.1 User unknown)

A dsn starting with 5 is a permanent failure: delete the address from your lists. A 4 code is temporary and Postfix retries it automatically; only drop the address if it keeps failing for several days.

For a daily summary of sent, deferred and bounced mail, install pflogsumm:

sudo apt install pflogsumm
sudo pflogsumm -d yesterday /var/log/mail.log | less

To receive a copy of every bounce notification that Postfix generates, set a recipient for them:

sudo postconf -e 'notify_classes = bounce, resource, software'
sudo postconf -e 'bounce_notice_recipient = postmaster@your_domain'
sudo systemctl reload postfix

Troubleshooting

Gmail shows "via another-domain" next to the sender. The DKIM signature uses a different domain than the From: header. Make sure OpenDKIM signs with your_domain and that your application uses an address @your_domain as From:.

DKIM fails with "body hash did not verify". Something modified the message after signing, usually a footer added by a mailing list or a content filter that runs after the milter. Sign as the last step before delivery.

opendkim-testkey reports "record not found". The TXT record is missing or still propagating, or it was created as mail._domainkey.your_domain.your_domain because the panel appends the zone automatically. Enter only mail._domainkey as the name in that case.

Mail to Outlook.com is rejected with a 5.7.x error mentioning the IP. Microsoft keeps its own reputation list. Use the delisting form linked in the rejection message and register the IP in SNDS.

Conclusion

Your Postfix server now has aligned reverse DNS, SPF, DKIM signatures and a DMARC policy, and you have a daily blocklist check plus a warm-up plan for new IPs. Keep an eye on DMARC reports and Postmaster Tools during the first weeks, then move DMARC to p=reject. Good next steps are adding OpenDMARC to verify inbound mail, publishing an MTA-STS policy for your domain and setting up a parser such as parsedmarc to read DMARC reports automatically.