If your application on a VPS needs to send emails (registration, passwords, notifications), Postfix is the standard choice. Configure it correctly, or your emails will end up in spam.
Installing Postfix
sudo apt update
sudo apt install -y postfix
# Type: "Internet Site"
# Mail name: mail.example.com
Basic /etc/postfix/main.cf
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
inet_interfaces = loopback-only # Local sending only
inet_protocols = ipv4
mydestination = $myhostname, localhost.$mydomain, localhost
relayhost =
smtpd_banner = $myhostname ESMTP
# Restrictions
smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination
Configuring DKIM via OpenDKIM
sudo apt install -y opendkim opendkim-tools
# Generate keys
sudo opendkim-genkey -s mail -d example.com -D /etc/opendkim/keys/example.com/
# Add to DNS TXT:
# mail._domainkey.example.com → content of mail.txt
# /etc/opendkim.conf:
Domain example.com
KeyFile /etc/opendkim/keys/example.com/mail.private
Selector mail
Testing delivery
sudo apt install -y mailutils
# Test
echo "Test email from VPS" | mail -s "Test" your@gmail.com
# Logs
sudo tail -f /var/log/mail.log
Alternative: SMTP Relay via an External Service
| Service | Free limit | Pros |
|---|---|---|
| SendGrid | 100 emails/day | High deliverability |
| Mailgun | 5000 emails/month | Good API |
| Amazon SES | 62000 emails/month | Cheapest option |
Common relay configuration mistakes
Most messages stuck in the Postfix queue on a ZevsHost VPS trace back to one of three causes.
status=deferred (delivery temporarily suspended: connect to gmail-smtp-in.l.google.com[...]:25: Connection timed out)
This is exactly how the outbound port block on 25/465/587 described above shows up in practice: Postfix tries to deliver the message directly, the connection hangs, and the message ends up stuck in the queue. If the port for an external relay hasn't been opened via a ticket yet, messages pile up in the queue instead of failing immediately with an error — which makes it confusing to diagnose.
SASL authentication failed; server smtp.sendgrid.net[...] said: 535 Authentication failed
Wrong login or password in /etc/postfix/sasl_passwd for the external relay, or the file wasn't rebuilt after editing: after any change, always run postmap /etc/postfix/sasl_passwd — Postfix reads the compiled .db database next to the file, not the file itself.
Client host rejected: cannot find your reverse hostname, [203.0.113.10]
The receiving server couldn't find a PTR record for your VPS's IP address and rejected the connection — a separate problem from the firewall block, and one that shows up even with the ports open. PTR is configured through hosting support, not through the domain's DNS panel.
Ports for sending mail
| Port | Protocol | When it's used | Encryption |
|---|---|---|---|
| 25 | SMTP | Server-to-server, direct delivery | STARTTLS optional |
| 465 | SMTPS | Sending from an application (implicit TLS) | TLS from the start of the connection |
| 587 | Submission | Recommended port for applications | STARTTLS required |
For relaying through an external service, use 587 or 465 — port 25 is only needed for direct delivery between mail servers, and on ZevsHost VDS it's closed by default along with the other two; see the warning at the top of the article.
When running your own Postfix doesn't fit
If your application sends marketing or bulk mail rather than transactional messages, Postfix plus an SMTP relay only solves part of the problem: without unsubscribe management, bounce handling, and sender reputation, a mailing quickly lands in recipients' spam filters. Use a dedicated mailing service's HTTP API for that instead of SMTP altogether. And if the application can already send mail through a mail provider's HTTP API, don't deploy Postfix at all — it's one more component to monitor and patch for no benefit.
How to verify the result
Check more than whether one message went out — check that the queue isn't growing and mail is actually flowing through the relay:
# Queue: the deferred count shouldn't keep growing
mailq | tail -20
# Test SASL authentication against the relay
swaks --to test@example.com --from you@example.com --server smtp.sendgrid.net:587 --auth LOGIN --auth-user apikey --auth-password YOUR_API_KEY --tls
# Check that the message actually went through the relay, not directly
grep "relay=" /var/log/mail.log | tail -5
After a successful send through the relay, the log line should contain relay=smtp.sendgrid.net[...]:587, not the recipient's IP directly. If you see the recipient's IP instead of the relay host, relayhost in main.cf didn't take effect — check the syntax and restart Postfix.