
Your mail server can look “fine” and still fail in ways you won’t catch right away. Messages can land in spam, providers can throttle outbound traffic, or TLS can break quietly after a renewal. This Postfix setup tutorial shows how to build a production-friendly SMTP setup on a VPS. It also covers the DNS and security controls that directly affect deliverability.
The goal is simple. You’ll set up authenticated submission on 587, opportunistic inbound SMTP on 25, modern TLS, sensible relay restrictions, and DNS you can prove (SPF, DKIM, DMARC, PTR). You’ll also get quick commands to verify each layer.
What you’ll build (and what you shouldn’t)
- OS: Ubuntu Server 24.04 LTS (commands also fit Debian 12 with minor path differences).
- SMTP stack: Postfix + Dovecot (SASL auth) + OpenDKIM (signing) + Let’s Encrypt (TLS).
- Mail style: This tutorial focuses on SMTP + submission. You can add IMAP later, but it’s not required to send reliably.
- Not included: Multi-node mail clusters or complex antispam farms. If you need that, you’re usually in dedicated-server territory.
Prerequisites checklist before you touch Postfix
Handle these first. If you skip them, you’ll chase vague bounces and “it works for me” deliverability issues.
- A VPS with a clean IPv4 and the ability to set rDNS/PTR.
- A domain/subdomain for mail (example used below:
mail.example.comfor the server, andexample.comfor recipients). - Open ports: 22/tcp (SSH), 25/tcp (SMTP), 587/tcp (submission). Optionally 465/tcp (smtps) and 143/993 for IMAP.
- One correct hostname set on the server:
mail.example.com.
If you’re spinning up a fresh mail node, start with a solid base. A HostMyCode VPS is a good fit for a single-domain SMTP server.
You can move to dedicated servers later if volume, compliance, or reputation separation requires it.
Step 1: Set hostname, FQDN, and time
Postfix uses your FQDN in several places. Receiving servers also validate it. Set it correctly before you install anything.
sudo hostnamectl set-hostname mail.example.com
hostname -f
Make sure /etc/hosts maps your primary IP to the FQDN (use your server IP):
sudo nano /etc/hosts
203.0.113.10 mail.example.com mail
127.0.0.1 localhost
Then fix clock drift. TLS handshakes and DKIM verification can fail in odd ways when time is wrong.
sudo timedatectl set-ntp true
timedatectl status
Step 2: Firewall rules that won’t break Let’s Encrypt or SMTP
Open only what you need. On Ubuntu, UFW works well and stays out of the way.
sudo ufw allow OpenSSH
sudo ufw allow 25/tcp
sudo ufw allow 587/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
If you want a practical reference for debugging blocked ports, keep this nearby. It’s most useful for 80/443 (Certbot) and 25/587 (mail): VPS firewall troubleshooting tutorial.
Step 3: Install Postfix + Dovecot SASL + OpenDKIM
Postfix handles SMTP. Dovecot provides SMTP AUTH (SASL). OpenDKIM signs outbound mail so recipients can verify it.
sudo apt update
sudo apt install -y postfix postfix-pcre dovecot-core dovecot-imapd opendkim opendkim-tools ca-certificates
During Postfix’s prompt, choose:
- General type: Internet Site
- System mail name:
example.com
Step 4: Configure Postfix (main.cf) for a secure baseline
Edit /etc/postfix/main.cf. The defaults are close. You still need to tighten identity, relay rules, TLS, and milters.
sudo nano /etc/postfix/main.cf
Use this as a clean baseline (adjust domains/IPs):
# Identity
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
mydestination = $myhostname, localhost.$mydomain, localhost
# Listen on all interfaces
inet_interfaces = all
inet_protocols = ipv4
# Mailbox (local delivery) - simple Maildir
home_mailbox = Maildir/
# Restrictions: accept authenticated, reject obvious garbage
smtpd_helo_required = yes
smtpd_helo_restrictions =
permit_mynetworks,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
reject_unknown_helo_hostname
smtpd_sender_restrictions =
permit_mynetworks,
reject_non_fqdn_sender,
reject_unknown_sender_domain
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_non_fqdn_recipient,
reject_unknown_recipient_domain,
reject_unauth_destination
# Don’t be an open relay
mynetworks = 127.0.0.0/8
# Size limits (tune for your use case)
message_size_limit = 30720000
mailbox_size_limit = 0
# Logging
maillog_file = /var/log/mail.log
# TLS - cert paths set later after Let’s Encrypt
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtpd_tls_loglevel = 1
smtp_tls_security_level = may
smtp_tls_loglevel = 1
# Stronger defaults (safe in 2026)
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
# Dovecot SASL for submission auth
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
# DKIM via OpenDKIM milter (configured later)
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
Pitfall: Don’t add your public IP ranges to mynetworks. That’s how “just for testing” turns into an open relay.
Step 5: Configure submission (587) and disable weak defaults
Next, adjust /etc/postfix/master.cf. Port 587 should require authentication and enforce encryption.
sudo nano /etc/postfix/master.cf
Find the submission service and set it like 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 smtpd_client_restrictions=permit_sasl_authenticated,reject
If you also want implicit TLS on 465, add (optional):
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
Step 6: Configure Dovecot SASL auth for Postfix
Dovecot exposes an auth socket that Postfix can use. Update these files:
sudo nano /etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:~/Maildir
sudo nano /etc/dovecot/conf.d/10-auth.conf
disable_plaintext_auth = yes
auth_mechanisms = plain login
Enable the Postfix auth socket in /etc/dovecot/conf.d/10-master.conf:
sudo nano /etc/dovecot/conf.d/10-master.conf
Locate the service auth block. Ensure this section exists:
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
You’ll restart services after the next steps. For now, keep going.
Step 7: Issue a TLS certificate with Let’s Encrypt
For SMTP submission, the certificate must match mail.example.com. HTTP-01 validation also requires port 80 to be reachable.
sudo apt install -y certbot
sudo certbot certonly --standalone -d mail.example.com --agree-tos -m admin@example.com --no-eff-email
Cert paths (standard on Ubuntu):
/etc/letsencrypt/live/mail.example.com/fullchain.pem/etc/letsencrypt/live/mail.example.com/privkey.pem
Point Postfix at them:
sudo postconf -e "smtpd_tls_cert_file=/etc/letsencrypt/live/mail.example.com/fullchain.pem"
sudo postconf -e "smtpd_tls_key_file=/etc/letsencrypt/live/mail.example.com/privkey.pem"
If renewals fail later, it’s usually firewall rules, port 80 reachability, or stale service bindings. This walkthrough is a solid fix guide: TLS certificate renewal troubleshooting tutorial.
Step 8: Set up OpenDKIM signing (keys, tables, and Postfix milter)
DKIM gives recipients cryptographic proof that your server can send for your domain. Without it, spam filters have less reason to trust you.
Create directories:
sudo mkdir -p /etc/opendkim/keys/example.com
sudo chown -R opendkim:opendkim /etc/opendkim
sudo chmod 750 /etc/opendkim/keys
Generate a 2048-bit key (a good baseline in 2026):
sudo -u opendkim opendkim-genkey -b 2048 -d example.com -s mail -D /etc/opendkim/keys/example.com
sudo chown opendkim:opendkim /etc/opendkim/keys/example.com/mail.private
sudo chmod 600 /etc/opendkim/keys/example.com/mail.private
Create these files:
sudo nano /etc/opendkim/KeyTable
mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com/mail.private
sudo nano /etc/opendkim/SigningTable
*@example.com mail._domainkey.example.com
sudo nano /etc/opendkim/TrustedHosts
127.0.0.1
localhost
mail.example.com
Configure OpenDKIM main config:
sudo nano /etc/opendkim.conf
Ensure these key lines exist (or match):
Syslog yes
UMask 002
Canonicalization relaxed/simple
Mode sv
SubDomains no
AutoRestart yes
AutoRestartRate 10/1h
Background yes
DNSTimeout 5
SignatureAlgorithm rsa-sha256
Socket inet:8891@localhost
PidFile /run/opendkim/opendkim.pid
UserID opendkim:opendkim
KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
ExternalIgnoreList /etc/opendkim/TrustedHosts
InternalHosts /etc/opendkim/TrustedHosts
Restart services:
sudo systemctl restart opendkim
sudo systemctl restart dovecot
sudo systemctl restart postfix
sudo systemctl enable opendkim dovecot postfix
Step 9: DNS records (MX, SPF, DKIM, DMARC, and PTR) you must publish
This is where many “my server is configured” setups fall apart. Gmail, Microsoft, Yahoo, and corporate gateways use these records to decide whether to trust you.
MX record
- Name:
@ - Type: MX
- Priority: 10
- Value:
mail.example.com
A record
- Name:
mail - Type: A
- Value: your VPS IPv4 (e.g.
203.0.113.10)
SPF
Start strict. Loosen it only if you also send through third-party services.
Type: TXT
Name: @
Value: v=spf1 mx -all
DKIM
Grab the public key from /etc/opendkim/keys/example.com/mail.txt:
sudo cat /etc/opendkim/keys/example.com/mail.txt
Publish the TXT record exactly as shown (selector mail in this tutorial).
DMARC
This conservative starter policy enables reporting. It won’t block mail while you validate alignment:
Type: TXT
Name: _dmarc
Value: v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; adkim=s; aspf=s
After you see consistent passes for a week or two, move to p=quarantine. Move to p=reject only after you control every sending source for the domain.
PTR (reverse DNS / rDNS)
Your VPS provider sets this. The PTR must map your IP back to the same hostname you present during SMTP.
- PTR:
203.0.113.10→mail.example.com
If you want a step-by-step flow, follow this: rDNS setup guide tutorial.
Step 10: Quick validation tests (SMTP, TLS, AUTH, DKIM, SPF, DMARC)
Don’t rely on “it seems to send.” Run a few checks locally. Then validate from the outside.
Confirm services are listening
sudo ss -ltnp | egrep '(:25|:587|:8891)'
Check TLS on submission (587)
openssl s_client -starttls smtp -connect mail.example.com:587 -servername mail.example.com </dev/null
You should see a valid certificate chain and your Let’s Encrypt certificate presented.
Check Postfix config sanity
sudo postfix check
sudo postconf -n
Send a test message from the server
sudo apt install -y bsd-mailx
printf "Subject: SMTP test\n\nHello from Postfix.\n" | sendmail -v you@your-personal-address.com
Verify DKIM signing locally
Check a delivered message header for DKIM-Signature:. If it’s missing, start here:
/var/log/mail.logfor milter connection errors- OpenDKIM socket
inet:8891@localhostmatches Postfix milters
Hardening checklist: keep your SMTP server from becoming a liability
- Block inbound auth on port 25. Only allow AUTH on 587/465.
- Disable plaintext auth. We set
disable_plaintext_auth = yesin Dovecot. - Keep submission encrypted. In
master.cf,smtpd_tls_security_level=encrypton 587. - Turn on fail2ban (optional but recommended). Watch for password sprays on 587.
- Monitor your queue. A growing deferred queue is often the first sign of DNS or reputation trouble.
Troubleshooting: the 6 failures you’ll hit first
1) “Relay access denied” when sending
This is expected if you try to send without AUTH on 587. You’ll also see it if your client uses port 25.
Use 587 with SMTP AUTH.
Confirm your mail client uses:
- Server:
mail.example.com - Port: 587
- Encryption: STARTTLS
- Auth: normal password (or app password if your client supports it)
2) TLS errors after renewal
If Postfix still points at an old certificate path, clients will fail the handshake. Verify the configured cert files. Then reload Postfix:
sudo postconf | egrep 'smtpd_tls_(cert|key)_file'
sudo systemctl reload postfix
3) DKIM “permerror” or “no key”
This almost always comes down to DNS formatting. Two common causes:
- TXT record wrapped or split incorrectly (some DNS panels auto-wrap; some need quotes).
- Wrong selector name (this tutorial uses
mail._domainkey).
4) SPF fails even though you added it
Make sure SPF is published at the root (@). Also confirm you have only one SPF TXT record.
Multiple SPF records break evaluation.
5) Outbound mail times out to major providers
This is usually network policy, not Postfix. Confirm outbound port 25 is allowed. Also check that your firewall isn’t blocking return traffic.
For a guided diagnosis (including TLS and 535 auth failures), use: SMTP troubleshooting tutorial.
6) Messages deliver but land in spam
Spam placement is usually a policy problem, not a “server down” problem. Work through these signals:
- PTR matches
mail.example.com - HELO/EHLO is FQDN
- SPF passes and includes your sending path
- DKIM passes and aligns with From domain
- DMARC has alignment (strict settings can break if you send from a different domain)
This workflow helps you isolate what’s actually failing: Email deliverability troubleshooting tutorial.
Operational hygiene: logs, queue checks, and safe updates
These three habits catch most problems before users notice.
Watch mail logs in real time
sudo tail -f /var/log/mail.log
Inspect and manage the queue
mailq
sudo postqueue -p
sudo postsuper -d ALL deferred
Keep packages patched
sudo apt update
sudo apt -y upgrade
sudo systemctl restart postfix dovecot opendkim
Summary: the “production ready” minimum for SMTP on a VPS
- Postfix configured to avoid relaying and enforce clean HELO/sender rules.
- 587 submission with auth + mandatory TLS.
- Let’s Encrypt certificate installed and renewals checked.
- OpenDKIM signing enabled and verified.
- DNS published: MX, SPF, DKIM, DMARC, and PTR (rDNS).
If you’re setting this up for a business domain, prioritize a stable IP and predictable performance. A managed VPS hosting plan from HostMyCode can cover patching, monitoring, and recovery. You still keep control of mail policy and DNS.
If you want a mail-ready server without guessing at firewall rules, rDNS, and base hardening, start with a HostMyCode VPS sized for your sending volume. If you’d rather not maintain the OS and core services yourself, choose managed VPS hosting and let our team handle the operational basics.
FAQ
Do I need port 465 if I already have 587?
No. Port 587 with STARTTLS is the standard for authenticated submission. Port 465 can help with legacy clients, but it’s optional.
Can I run this on shared hosting?
Not realistically. Shared hosting usually doesn’t allow you to run Postfix as a system service or set rDNS. Use a VPS or dedicated server for a real SMTP setup.
What’s the fastest way to confirm rDNS is correct?
From any Linux/macOS shell, run: dig -x 203.0.113.10 +short. It should return mail.example.com..
Why does my DMARC pass but messages still hit spam?
Authentication isn’t reputation. If SPF/DKIM/DMARC pass but spam placement persists, warm up volume gradually, keep bounce rates low, and make sure PTR/HELO alignment is correct.
Should I use “-all” in SPF immediately?
If your domain sends mail only from this server, yes. If you also send from Google Workspace, Microsoft 365, or a marketing platform, you must include those senders before enforcing -all.