
Most VPS mail failures aren’t “Postfix problems.” They’re identity problems. Receivers want consistent signals: TLS, a sane HELO, matching rDNS, and SPF/DKIM alignment.
This walkthrough shows you how to set up Postfix SMTP on an Ubuntu VPS for dependable outbound delivery. Then you’ll add SPF + DKIM so Gmail, Microsoft, and Yahoo can verify what you send.
By the end, you’ll have a working submission endpoint on 587 (plus optional 465) for your apps. You’ll also have DKIM signing for your domain and a repeatable verification checklist to run after any change.
What you’ll build (and what you need)
- Ubuntu VPS (Ubuntu 22.04 LTS or 24.04 LTS both work in 2026)
- Postfix as an outbound SMTP server with TLS and authenticated submission
- OpenDKIM for DKIM signing
- DNS records: SPF, DKIM, and basic DMARC
- Practical tests: openssl s_client, swaks, and header inspection
If you’re sending production mail, keep this on a clean host or a dedicated mail VPS. You can run it on a general web server. Deliverability problems tend to spill into everything else.
If you want a clean IP and predictable outbound reputation, start with a HostMyCode VPS. Keep the role narrow: outbound SMTP + DKIM only.
Before you touch Postfix: rDNS, hostname, and port reality
Handle these first. A messy identity creates a shaky reputation from day one.
1) Confirm outbound SMTP ports and your provider policy
Many providers restrict TCP/25 by default to limit abuse. Your apps can submit on 587, but Postfix still delivers to recipient MX servers over 25.
Check connectivity:
nc -vz gmail-smtp-in.l.google.com 25
nc -vz outlook-com.olc.protection.outlook.com 25
You’re looking for “succeeded.” If you see timeouts, ask your VPS provider to enable outbound TCP/25 for legitimate mail.
Keep volumes reasonable. Don’t run marketing blasts from a VPS.
2) Set a real FQDN hostname (must match your server identity)
Pick a hostname like mail.example.com and set it:
sudo hostnamectl set-hostname mail.example.com
hostname -f
hostname -f should return the same FQDN.
3) Set forward DNS and rDNS (PTR) to match
- Create an A record for
mail.example.compointing to your VPS IPv4. - Set PTR/rDNS on your VPS IP to
mail.example.com(set this in your hosting control panel, not your DNS zone).
Verify from the VPS:
dig +short mail.example.com A
dig +short -x YOUR.SERVER.IP
The PTR lookup should return mail.example.com.
Aligning PTR + HELO + hostname is one of the fastest ways to reduce “mystery spam folder” behavior.
If you want a clean domain setup workflow, HostMyCode’s domains service keeps record edits straightforward while you iterate on SPF/DKIM and future key rotations.
Install Postfix and OpenDKIM on Ubuntu
Update packages, then install the pieces we’ll use:
sudo apt update
sudo apt -y install postfix opendkim opendkim-tools libsasl2-modules mailutils ca-certificates
During the Postfix prompt:
- Select Internet Site
- System mail name: use example.com (your sending domain) or mail.example.com (either works, but we’ll set it explicitly in config)
Open the firewall for submission
Allow SSH and the submission ports. (Inbound port 25 is optional if you only send mail; you can leave it closed.)
sudo ufw allow OpenSSH
sudo ufw allow 587/tcp
sudo ufw allow 465/tcp
sudo ufw enable
sudo ufw status
If you don’t use UFW, apply your normal firewall baseline.
If you want a reference setup for web hosts, see: Set up UFW firewall on Ubuntu for a web server.
Configure TLS for Postfix (Let’s Encrypt, simple and correct)
Your apps should submit mail over TLS using a real certificate. The clean path is a Let’s Encrypt cert for mail.example.com.
Get a certificate
If Certbot is already in your stack, reuse it. Otherwise, install it and request a standalone cert:
sudo apt -y install certbot
sudo certbot certonly --standalone -d mail.example.com
Certificates typically end up here:
/etc/letsencrypt/live/mail.example.com/fullchain.pem/etc/letsencrypt/live/mail.example.com/privkey.pem
For renewal failures, chain issues, and common name/SAN mistakes, this is a solid troubleshooting reference: TLS setup guide tutorial (2026).
Lock Postfix to authenticated outbound mail (submission on 587/465)
We’re building an outbound submission server for websites and apps. The goal is SMTP AUTH + TLS, with relay protection by default.
Edit /etc/postfix/main.cf
Open the file:
sudo nano /etc/postfix/main.cf
Add or adjust these settings (replace example.com and mail.example.com):
# Identity
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
mydestination = localhost
inet_interfaces = all
inet_protocols = all
# Banner and HELO hygiene
smtpd_banner = $myhostname ESMTP
smtp_helo_name = $myhostname
# TLS (server-side)
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_loglevel = 1
smtpd_tls_received_header = yes
# TLS (client-side when Postfix delivers outbound)
smtp_tls_security_level = may
smtp_tls_loglevel = 1
# SASL auth for submission
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
# We'll use Cyrus SASL with saslauthd-less local auth via sasldb (simple for outbound-only)
# Alternatively integrate Dovecot; for this tutorial we use sasldb + Cyrus
# Restrictions: don't become a relay
smtpd_recipient_restrictions =
permit_sasl_authenticated,
permit_mynetworks,
reject_unauth_destination
# Reasonable limits
message_size_limit = 26214400
Important: The block above mentions dovecot for SASL type, but we aren’t installing Dovecot in this outbound-only build. Use Cyrus SASL via sasldb instead.
Replace these two lines:
smtpd_sasl_type = cyrus
smtpd_sasl_path = smtpd
Enable submission services in /etc/postfix/master.cf
Edit:
sudo nano /etc/postfix/master.cf
Find the submission and smtps sections. Make sure they’re enabled and match this (uncomment if needed):
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
This enforces STARTTLS + auth on 587, and implicit TLS + auth on 465.
Configure Cyrus SASL for Postfix
Create the Postfix SASL config:
sudo mkdir -p /etc/postfix/sasl
sudo nano /etc/postfix/sasl/smtpd.conf
Paste:
pwcheck_method: auxprop
auxprop_plugin: sasldb
mech_list: PLAIN LOGIN
Add the Postfix user to the sasl group:
sudo adduser postfix sasl
Create an SMTP user (credentials your apps will use)
Create a dedicated login for SMTP. Don’t reuse your Linux admin account.
sudo saslpasswd2 -c -u example.com smtpuser
sudo sasldblistusers2
You should see smtpuser@example.com listed.
Restart and verify Postfix is listening
sudo systemctl restart postfix
sudo systemctl enable postfix
sudo ss -ltnp | egrep ':(25|465|587)\b'
You should see listeners on 587 and 465.
Port 25 may also be listening locally for delivery; that’s normal.
Set up DKIM signing with OpenDKIM
SPF is a useful first signal, but DKIM is the signature that survives forwarding. It also carries your domain identity.
We’ll sign mail from example.com using selector s1.
Create the DKIM key
sudo mkdir -p /etc/opendkim/keys/example.com
sudo chown -R opendkim:opendkim /etc/opendkim
sudo chmod 700 /etc/opendkim/keys
sudo opendkim-genkey -D /etc/opendkim/keys/example.com -d example.com -s s1
sudo chown opendkim:opendkim /etc/opendkim/keys/example.com/s1.private
sudo chmod 600 /etc/opendkim/keys/example.com/s1.private
This generates:
s1.private(private key, stays on the server)s1.txt(public key DNS record template)
Configure OpenDKIM
Edit:
sudo nano /etc/opendkim.conf
Ensure these lines exist (add if missing):
Syslog yes
UMask 002
Canonicalization relaxed/simple
Mode sv
SubDomains no
AutoRestart yes
AutoRestartRate 10/1h
Socket local:/run/opendkim/opendkim.sock
PidFile /run/opendkim/opendkim.pid
UserID opendkim
KeyTable /etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
TrustedHosts /etc/opendkim/trusted.hosts
Create KeyTable, SigningTable, TrustedHosts
sudo nano /etc/opendkim/key.table
s1._domainkey.example.com example.com:s1:/etc/opendkim/keys/example.com/s1.private
sudo nano /etc/opendkim/signing.table
*@example.com s1._domainkey.example.com
sudo nano /etc/opendkim/trusted.hosts
127.0.0.1
localhost
mail.example.com
Ensure the OpenDKIM socket directory exists
Ubuntu usually creates this at service start. Still, it’s quick to confirm:
sudo mkdir -p /run/opendkim
sudo chown opendkim:opendkim /run/opendkim
Connect Postfix to OpenDKIM (milter)
Point Postfix at the OpenDKIM socket:
sudo postconf -e 'milter_default_action=accept'
sudo postconf -e 'milter_protocol=6'
sudo postconf -e 'smtpd_milters=local:/run/opendkim/opendkim.sock'
sudo postconf -e 'non_smtpd_milters=$smtpd_milters'
Restart services and confirm OpenDKIM comes up cleanly:
sudo systemctl restart opendkim
sudo systemctl restart postfix
sudo systemctl status opendkim --no-pager
Add SPF, DKIM, and DMARC to your DNS (alignment matters)
Now publish the records receivers use to validate your mail. You’ll add three records at your DNS host.
If you manage DNS through HostMyCode, add them in the domains dashboard.
DKIM record
Print the DKIM DNS value:
sudo cat /etc/opendkim/keys/example.com/s1.txt
You’ll get something like:
s1._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; p=..." )
Create a TXT record:
- Name/Host:
s1._domainkey - Value: the
v=DKIM1; ... p=...string (remove quotes and parentheses if your DNS UI expects raw text)
Wait for DNS propagation, then verify:
dig +short TXT s1._domainkey.example.com
SPF record
If this VPS is your only sender, keep SPF tight:
example.com. IN TXT "v=spf1 ip4:YOUR.SERVER.IP -all"
Create one TXT record at the root (@ in many DNS panels). Verify:
dig +short TXT example.com | grep spf
Common mistake: publishing multiple SPF TXT records. You should have exactly one.
If you also send through Google Workspace, Mailgun, etc., merge everything into a single SPF string.
DMARC record (basic but effective)
DMARC tells receivers what to do when SPF/DKIM fail and sends you reports. Start in monitoring mode:
_dmarc.example.com. IN TXT "v=DMARC1; p=none; adkim=s; aspf=s; rua=mailto:dmarc-reports@example.com"
Make sure dmarc-reports@example.com goes to a mailbox you actually read.
After a week or two of clean alignment, you can move to p=quarantine or p=reject. Only do that after you confirm every legitimate sender is covered.
If you want a DNS-only walkthrough with extra validation steps, this guide stays focused on the records and checks: Set Up DKIM, SPF, And DMARC For A Domain.
Test SMTP AUTH + TLS from the outside (don’t guess)
Test from your laptop first. It’s the fastest way to catch firewall rules, certificate issues, and auth mistakes.
1) Confirm TLS works on 587
openssl s_client -starttls smtp -connect mail.example.com:587 -servername mail.example.com
Look for:
- A valid certificate chain (Let’s Encrypt)
250-AUTHin the server capabilities after EHLO
2) Send a test email with swaks
Install swaks on your local machine, or on the VPS if that’s easier. On Ubuntu:
sudo apt -y install swaks
Then run (replace values):
swaks --to you@gmail.com \
--from test@example.com \
--server mail.example.com:587 \
--auth LOGIN --auth-user smtpuser@example.com --auth-password 'YOUR_PASSWORD' \
--tls
If something fails, check the logs while the error is still fresh:
sudo journalctl -u postfix -n 200 --no-pager
sudo journalctl -u opendkim -n 200 --no-pager
Verify DKIM is actually signing (and aligned)
Delivery isn’t the finish line. You want proof that SPF and DKIM both pass.
You also want the domains to match what you expect.
Check in Gmail
- Open the message in Gmail
- Click “More” → “Show original”
- Confirm: DKIM: PASS and SPF: PASS
Also confirm the domain in DKIM-Signature: d=example.com matches your From domain.
That match is what “alignment” means in practice.
Command-line DKIM check (DNS visibility)
From any machine:
opendkim-testkey -d example.com -s s1 -vvv
If you see “record not found,” the TXT record is wrong or DNS hasn’t propagated yet.
“Key not secure” often points to formatting problems in the published DKIM value.
Make your apps use the server safely (WordPress, Laravel, scripts)
Always submit through 587 with TLS and authentication. Don’t have your app code connect directly to remote MX servers.
Recommended SMTP settings
- Host:
mail.example.com - Port:
587 - Encryption: STARTTLS
- Username:
smtpuser@example.com - Password: the SASL password you set
- From: a real address on
@example.com(match your DKIM signing domain)
If WordPress mail is unreliable, routing it through this server usually stabilizes delivery.
If you prefer a relay-style approach tailored to WordPress, this related guide is good context: Set Up Postfix SMTP Relay on Ubuntu for WordPress.
Operational hardening: rate limits, log visibility, and updates
An outbound SMTP box becomes a liability the moment a site gets compromised. Assume credentials could leak, and plan for it.
Control throughput and make spikes obvious.
Basic outbound rate limits (practical defaults)
Add these to /etc/postfix/main.cf to limit damage if credentials escape:
smtp_destination_rate_delay = 1s
smtp_destination_concurrency_limit = 5
anvil_rate_time_unit = 60s
smtpd_client_message_rate_limit = 60
Reload Postfix:
sudo postfix reload
Watch the queue and bounce reasons
mailq
postqueue -p
sudo tail -n 200 /var/log/mail.log
If you repeatedly see status=bounced with “blocked” or “policy,” treat it as a signal problem first.
Common causes include PTR mismatch, a bad HELO, missing DKIM, or sending patterns that look automated and abusive.
Keep security updates automatic
Mail services are exposed, so patch regularly. On Ubuntu, unattended upgrades are a sensible baseline:
Set Up Automated Security Updates on Ubuntu VPS
Common mistakes (and fast fixes)
- PTR doesn’t match your hostname: set rDNS to
mail.example.com, and make sure the A record points back to the same IP. - SPF has multiple records: merge into one. Many receivers treat multiple SPF records as a permerror.
- DKIM record pasted with broken quotes: some DNS panels wrap strings; confirm the published TXT is exactly what
digreturns. - Using “From: noreply@” but signing a different domain: align the From domain with
d=in DKIM and the SPF domain. - Port 587 works, but mail never arrives: outbound TCP/25 is blocked or the IP is listed/tainted. Confirm with
ncand read the bounce text.
Summary: a clean outbound SMTP setup you can trust
You now have Postfix accepting authenticated mail on 587/465 with TLS, plus DKIM signing via OpenDKIM. You also published DNS records (SPF/DKIM/DMARC) that align with your From domain.
Keep three habits: maintain PTR/hostname consistency, keep SPF to a single record, and confirm DKIM PASS from a real inbox after changes.
If you want a stable platform for mail alongside web workloads, run this on a dedicated instance. A managed VPS hosting plan from HostMyCode also helps if you’d rather offload patching and service health checks while you focus on applications.
If your website emails matter (password resets, invoices, alerts), run outbound mail on infrastructure you can inspect and control. Start with a clean IP on a HostMyCode VPS, or choose managed VPS hosting if you want help keeping the box patched and the services stable.
FAQ
Do I need inbound port 25 open for this setup?
Not for client submission. Your apps use 587/465. But your server still needs outbound TCP/25 to deliver to recipient MX servers. Inbound 25 is only needed if you want to receive mail directly.
Why does SPF pass but my mail still goes to spam?
SPF alone doesn’t carry much weight. Receivers look hard at DKIM alignment, PTR/HELO consistency, sending patterns, and domain/IP reputation. Start by confirming DKIM PASS and a matching PTR.
Can I reuse the same DKIM key forever?
You can, but rotation is smart—at least annually, or immediately after any suspected compromise. Publish a second selector (for example s2), switch signing to it, then remove the old record later.
Should I set DMARC to reject immediately?
No. Start with p=none, read the reports, then move to quarantine or reject once you’ve confirmed every legitimate sender is aligned.
Is this enough for bulk newsletters?
No. A VPS SMTP server is best for transactional mail at modest volume. For newsletters, use a provider built for bulk sending or a dedicated deliverability stack with warmed IPs and feedback loop handling.