Servers on residential connections, dynamic IPs or generic cloud VMs rarely have usable reverse DNS or a sending reputation, and many providers block outbound port 25 outright (see DNS and Mail Routing). Mail sent directly from them gets rejected or spam-foldered however correct the Postfix config is. The practical fix is not to send directly at all. Instead, relay everything through a smart host that already has a reputation: Gmail, a transactional provider (Mailgun, SendGrid, Postmark, Amazon SES), Proton Mail's SMTP submission, or your own well-run mail server.
The null client pattern¶
A null client only ever sends. It receives nothing from the network and delivers nothing locally. This is the right model for application servers, a Raspberry Pi, a NAS or a hypervisor that only needs to email alerts.
# /etc/postfix/main.cf
myhostname = host01.domain.tld
myorigin = $mydomain
mydestination = # nothing is local
inet_interfaces = loopback-only # accept only locally submitted mail
inet_protocols = ipv4
mynetworks = 127.0.0.0/8 [::1]/128
relayhost = [mail.domain.tld]:587
local_transport = error:local delivery is disabled
Key points:
inet_interfaces = loopback-only: nothing external can connect. Postfix accepts only mail submitted locally viasendmail/mailorlocalhost:25.mydestination =(empty) together withlocal_transport = error:…means nothing is ever delivered locally. Mail torootmust be redirected (see Rewriting sender and recipient addresses below), or it bounces with a clear error instead of vanishing into an unread/var/mail/root.relayhost: everything goes here regardless of destination. Square brackets mean "connect to this host directly, don't look up its MX". That's what you want for a submission service.
Postfix documents this pattern as the null client in STANDARD_CONFIGURATION_README.
Do you need a daemon at all? msmtp¶
A null-client Postfix is still a running daemon (master, pickup, qmgr, smtp), just configured to do very little. That's right when the host is standalone. But if what you want is "let this host's local tools hand mail to a relay that already exists nearby", you don't need a second MTA.
A common case is a containerized Postfix already acting as the relay on a host, and you want the host itself (cron, mail, system alerts) to send through it too. A second full Postfix on the host only to forward to the container next door is redundant: another daemon, queue and config to maintain, and another process wanting port 25.
msmtp is built for this. It isn't a service. It runs once per message, connects, sends and exits. There's no listening socket, no queue directory, and nothing competing for port 25.
sudo apt purge postfix # if a full Postfix was installed for this purpose
sudo apt install msmtp msmtp-mta # dnf install msmtp on RHEL-family (EPEL)
msmtp-mta symlinks /usr/sbin/sendmail to msmtp, so anything that calls sendmail (the mail command, cron, application error mailers) works unchanged.
# /etc/msmtprc
defaults
syslog LOG_MAIL
aliases /etc/aliases # lets msmtp map root: etc. to real addresses
auth off
tls off
account relay
host 127.0.0.1 # the container's published port, or its address on the Docker network
port 25
from [email protected]
allow_from_override off # always send as $from, whatever the caller sets
account default : relay
The relay container must accept this host as a client. That means either its mynetworks includes the address the host connects from, or its port 25 is published only on 127.0.0.1. The container stays the single place that holds the upstream relay credentials. No duplicate SASL config lives on the host.
echo "test" | mail -s "host via relay container" [email protected]
docker logs -f postfix # the message arrives like any other client's
When to use which: a standalone host with no relay nearby should run a minimal null-client Postfix (it queues and retries if the smart host is down; msmtp just fails). Use msmtp when a working relay is one hop away and would do the queueing anyway.
Relaying via Gmail¶
Gmail's SMTP servers can relay modest volumes from your own account. Google applies daily sending limits, and it rewrites the From: to the authenticated account unless the address is configured as a verified "Send mail as" alias. This fits personal alerting, not bulk mail.
# /etc/postfix/main.cf (null client settings from above, plus:)
relayhost = [smtp.gmail.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_tls_security_options = noanonymous
smtp_tls_security_level = encrypt # never send credentials without TLS
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt # RHEL: /etc/pki/tls/certs/ca-bundle.crt
# /etc/postfix/sasl_passwd (the key must match relayhost exactly, brackets and port included)
[smtp.gmail.com]:587 [email protected]:abcdefghijklmnop
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo systemctl reload postfix
The password must be an app password (16 characters, generated under Google Account → Security → App passwords). App passwords require 2-Step Verification, and Google no longer accepts normal account passwords from SMTP clients. Workspace administrators can disable app passwords. In that case use Google Workspace's dedicated SMTP relay service, which authenticates by IP or by account, instead.
Relaying via Mailgun (or any transactional provider)¶
Same shape, different provider. This is the general pattern for any SMTP relay service: Mailgun, SendGrid, Postmark, Amazon SES's SMTP interface, Brevo and others.
relayhost = [smtp.mailgun.org]:587 # EU region: smtp.eu.mailgun.org
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_tls_security_options = noanonymous
smtp_sasl_mechanism_filter = plain, login
smtp_tls_security_level = encrypt
# /etc/postfix/sasl_passwd
[smtp.mailgun.org]:587 [email protected]:MAILGUN_SMTP_PASSWORD
sudo chmod 600 /etc/postfix/sasl_passwd && sudo postmap /etc/postfix/sasl_passwd && sudo systemctl reload postfix
echo "Test" | mail -s "Test via Mailgun" [email protected]; sudo tail -F /var/log/mail.log
The SMTP credentials are per sending domain (mg.domain.tld here), created in the provider's dashboard. Complete the provider's domain verification (SPF include, DKIM keys and usually a tracking CNAME) before sending, or receivers will reject the mail.
Relaying via Proton Mail (SMTP submission)¶
Proton Mail offers an SMTP submission service for sending from custom-domain addresses. It drops into the same pattern:
relayhost = [smtp.protonmail.ch]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
# /etc/postfix/sasl_passwd
[smtp.protonmail.ch]:587 [email protected]:PROTON_SMTP_TOKEN
Proton-specific points:
- It requires a paid plan with a custom domain. The credential is a per-address SMTP token generated in Proton's settings, not your account password.
- Only addresses the token belongs to can be used as the sender, so pair it with
smtp_generic_maps(below) soroot@host01becomes that address. - Your domain's SPF and DKIM must be set up in Proton's domain settings, as with any provider.
SMTP submission is not Proton Mail Bridge. Bridge is a local application that exposes a full IMAP + SMTP server on
127.0.0.1so a desktop client can read and send. SMTP submission is send-only, talks to Proton's public endpoint, and needs no local software, which is why it's the one that fits a headless relay. See Proton Mail Bridge in the mail clients section for Bridge.
Relaying on port 465 (implicit TLS)¶
Some providers offer only port 465, or prefer it. Postfix needs to start TLS immediately instead of using STARTTLS:
relayhost = [smtp.provider.tld]:465
smtp_tls_wrappermode = yes
smtp_tls_security_level = encrypt
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
Without smtp_tls_wrappermode = yes, connections to 465 hang and time out, because Postfix waits for a plaintext greeting the server never sends.
Rewriting sender and recipient addresses¶
A null client's mail naturally comes from addresses like [email protected] or [email protected]. Smart hosts often reject senders they don't recognize, and replies to them go nowhere. smtp_generic_maps rewrites addresses only as mail leaves via SMTP, which is exactly the null-client case:
smtp_generic_maps = hash:/etc/postfix/generic
# /etc/postfix/generic
[email protected] [email protected]
@host01.domain.tld [email protected]
@localhost [email protected]
sudo postmap /etc/postfix/generic && sudo postfix reload
To get system mail addressed to root to a human, add a virtual alias (this works even with no local delivery):
virtual_alias_maps = inline:{ [email protected], [email protected] }
Different relays for different senders¶
One host can relay mail through different providers depending on the sender address. For example, personal mail goes via Gmail and alerts go via Mailgun:
sender_dependent_relayhost_maps = hash:/etc/postfix/sender_relay
smtp_sender_dependent_authentication = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
relayhost = [smtp.mailgun.org]:587 # fallback for everything else
# /etc/postfix/sender_relay
[email protected] [smtp.gmail.com]:587
@domain.tld [smtp.mailgun.org]:587
# /etc/postfix/sasl_passwd: sender keys are tried first, then relay host keys
[email protected] [email protected]:APP_PASSWORD
[smtp.mailgun.org]:587 [email protected]:MAILGUN_SMTP_PASSWORD
Handling relay credentials¶
sasl_passwdand its compiled.db/.lmdbhold plaintext-equivalent secrets. Make bothroot:root 0600. Postfix reads the compiled map before it drops privileges, so it doesn't need group access.- Once the map is compiled, Postfix only reads the compiled file, so the plaintext source can be removed. Keep the source of truth in a secrets manager and regenerate at deploy time, for example:
bash
umask 077
pass show infra/postfix/sasl_passwd > /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd && shred -u /etc/postfix/sasl_passwd
postfix reload
- Use a dedicated credential per host or relay (app password, SMTP token, per-domain SMTP user), never your main account password, so one compromised host can be revoked on its own.
Checklist: relayhost setups¶
- [ ]
inet_interfaces = loopback-onlyunless this host also legitimately receives mail - [ ] Local delivery disabled (
mydestination =,local_transport = error:…) androotaliased to a real address - [ ]
relayhostin[brackets]with the correct port, andsmtp_tls_wrappermode = yesif that port is 465 - [ ]
smtp_tls_security_level = encryptso credentials never go over plaintext - [ ] The
sasl_passwdkey matchesrelayhostexactly; the map is compiled and permissions are0600 - [ ] Sender addresses rewritten to something the provider accepts (
smtp_generic_maps) - [ ] SPF/DKIM/DMARC for the sending domain authorize the provider (see Email Authentication and Anti-Spam)
- [ ] Test mail delivered and passing authentication (
Authentication-Results:header showsspf=pass dkim=pass dmarc=pass)