Andrew Mercer
on this page

As covered in the fundamentals section (DNS and mail routing), servers on residential connections, dynamic IPs, or generic cloud VMs almost never have usable reverse DNS, and get their mail silently dropped or spam-flagged by major receivers no matter how correctly Postfix itself is configured. The practical fix, used repeatedly across the personal notes distilled into this guide, is to not send directly at all — instead relay everything through a smart host (a relayhost) that already has sending reputation: Gmail's SMTP servers, or a transactional email provider like Mailgun.

The "null client" pattern

A null client is a machine that only ever sends mail — it receives nothing from the network and delivers nothing locally. It's the right model for a server/app host, a Raspberry Pi, a home NAS, etc. that just needs to email alerts or notifications somewhere, with no local mailboxes at all:

# /etc/postfix/main.cf
myhostname = hostname.domain.tld
myorigin = $mydomain
mynetworks = 127.0.0.0/8, [::1]/128
relayhost = mx.domain.tld
inet_interfaces = loopback-only
local_transport = error: local delivery disabled

Key points: - inet_interfaces = loopback-only — nothing external can connect to this Postfix instance at all; it only accepts mail submitted locally (e.g. via the mail command or an application using sendmail/localhost:25). - local_transport = error: local delivery disabled — explicitly refuses local delivery attempts rather than silently misbehaving if something tries to send to a local-looking address. - relayhost — everything gets handed off here regardless of destination domain.

(Postfix's own documentation has a dedicated reference for this pattern: STANDARD_CONFIGURATION_README.html#null_client.)

Do you actually need a daemon at all?

A Postfix null client is still a running daemon — master, smtpd/pickup, qmgr, smtp, all present, just configured to do almost nothing (no inbound listening, no local delivery). That's the right tool when the sending host is meaningfully separate infrastructure from wherever the relay work happens. But if what you actually want is just: "let this one host's local tools (mail, cron jobs, app error notifications, etc.) hand off mail somewhere" and a relay already exists nearby — you don't need a second MTA daemon on that host at all.

This comes up directly with a common setup: a containerized Postfix already acting as the relay/smart-host client on a bare-metal server, and you want the bare-metal host itself to also be able to send mail (not just containers on it). Running a second full Postfix on the host, purely to immediately hand everything to the container next door, is redundant — it's a daemon, a queue, and a config to maintain for a job that's really just "run a program once per outgoing message."

msmtp — a sendmail-compatible shim, no daemon

msmtp is built for exactly this: it isn't a service — it's a small program invoked once per message, which connects out, sends, and exits. No listening socket, no queue directory, no systemctl status to check, and critically nothing competing with your container for port 25 on the host.

sudo apt purge postfix        # if you'd already installed a full Postfix for this — remove it, you don't need it
sudo apt install msmtp msmtp-mta

msmtp-mta is the package that matters: it symlinks /usr/sbin/sendmail to msmtp, so anything on the host that shells out to sendmail — the mail/mailx command, cron's mail-on-error behavior, most application error-notification code — transparently works with no changes, believing it's talking to a real MTA.

Configure /etc/msmtprc to point at your existing container:

defaults
auth           off
tls            off
host           127.0.0.1
port           25
from           you@yourdomain.tld

account        default
host           127.0.0.1
port           25

Since your container's Postfix already has the relevant address in its mynetworks (or is simply reachable on 127.0.0.1/the host's own address if the container's port 25 is published to the host), this routes host-local mail straight into the container — which already knows how to authenticate and relay onward to your actual provider. No duplicate relayhost/SASL config needed on the host; the container remains the single place that holds those credentials.

Test:

echo "test" | mail -s "host via container" [email protected]
docker logs postfix -f

You should see the message arrive in the container's log exactly as if it came from any other host on the LAN relaying through it.

When to reach for this vs. a real null-client Postfix on the host: if the host is standalone (no other Postfix instance already doing relay work nearby), a minimal null-client Postfix as described above is perfectly reasonable and keeps everything self-contained. msmtp is the better fit specifically when — as here — a working relay is already one hop away and running a second daemon would just be forwarding to the first one for no benefit.

(If the container in question is your own containerized Postfix build, its Dockerfile/compose setup and any relayhost config baked into the image live in your containers repo, under mail/postfix — that's the actual source of truth for what the container is configured to relay through, separate from this guide.)

Relaying via Gmail (SMTP AUTH smart host)

Gmail's SMTP servers can act as a relayhost for a small number of outbound emails (this is subject to Google's own sending limits and account policies — it's a fine fit for low-volume alerting, not for bulk mail). The full pattern:

# /etc/postfix/main.cf
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = hostname.domain.tld
mydomain = domain.tld
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = $mydomain
mydestination = [email protected], hostname, localhost.localdomain, localhost
relayhost = [smtp.gmail.com]:587
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = loopback-only
local_transport = error: local delivery disabled

smtp_use_tls = yes
smtp_sasl_auth_enable = yes
smtp_sasl_security_options =
smtp_sasl_password_maps = hash:/etc/postfix/sasl_password
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt

The credentials live in a separate lookup table, keyed by the relayhost:

#  /etc/postfix/sasl_password
[smtp.gmail.com]:587    username@gmail.com:app_password
postmap /etc/postfix/sasl_password
chmod 600 /etc/postfix/sasl_password

Note on Gmail app passwords: if the Google account has 2-Step Verification enabled (as most should), you can't use the normal account password here — you need an app password generated specifically for this purpose. Using the real account password will simply fail authentication.

Square brackets around the hostname in relayhost = [smtp.gmail.com]:587 mean "don't look up the MX record for this — connect to this hostname directly." Since you're relaying to a specific submission service, not doing MX-based routing, this is exactly what you want.

Relaying via Mailgun (or any transactional-email SMTP provider)

Same shape, different provider — this is the general pattern for any SMTP-relay-as-a-service provider (Mailgun, SendGrid, Postmark, SES's SMTP interface, etc.):

# Debian/Ubuntu unattended install, pre-seeded straight to a Satellite config:
debconf-set-selections <<< "postfix postfix/main_mailer_type select Satellite system"
debconf-set-selections <<< "postfix postfix/mailname string $HOSTNAME"
debconf-set-selections <<< "postfix postfix/relayhost string smtp.mailgun.org"
sudo apt-get -y install postfix mailutils
# /etc/postfix/main.cf
myhostname = hostname.domain.tld
myorigin = $mydomain
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
relayhost = smtp.mailgun.org
inet_interfaces = loopback-only
local_transport = error: local delivery disabled

# MailGun SASL auth
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 = AUTH LOGIN
# /etc/postfix/sasl_passwd
smtp.mailgun.org    postmaster@mg.domain.tld:MAILGUN_SMTP_PASSWORD
chmod 0600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
systemctl restart postfix

Once sasl_passwd.db has been generated, it can be copied to other hosts as-is — and the source sasl_passwd file (which contains the plaintext password) can then be deleted, since Postfix only ever reads the compiled .db:

rm /etc/postfix/sasl_passwd

Test and watch the log in one line:

echo "Test" | mail -s "Test" [email protected]; tail -F /var/log/mail.log

Relaying via Proton Mail

Proton Mail offers a dedicated SMTP submission service for outbound-only relaying from a custom-domain address — same shape as Mailgun above, just a different provider, and worth knowing about specifically because it's easy to confuse with the separate Bridge product (see the callout below).

# /etc/postfix/main.cf
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
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
# /etc/postfix/sasl_passwd
[smtp.protonmail.ch]:587    you@yourdomain.tld:PROTON_SMTP_TOKEN
chmod 0600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
systemctl restart postfix

A few things specific to Proton, as of current documentation: - This requires a paid Proton Mail plan with a custom domain address — it's not available on the free tier. The credential isn't your normal account password; it's a dedicated SMTP token generated per-address in Proton's settings, used only for this purpose. - Authentication methods are PLAIN or LOGIN; encryption is STARTTLS on port 587 — same as the Gmail/Mailgun patterns above, so it drops into the same smtp_sasl_*/smtp_tls_* shape. - As with Gmail, SPF for your sending domain needs to authorize Proton's sending infrastructure or receivers may still flag the mail despite a technically correct relay config (see the email-fundamentals authentication doc).

Don't confuse SMTP submission with Proton Mail Bridge. Bridge is a completely different product: a local desktop application (also paid-plan-only) that runs a small IMAP and SMTP server on 127.0.0.1, decrypting/encrypting against Proton's end-to-end-encrypted storage so a normal mail client can read and send Proton Mail as if it were a regular IMAP account. SMTP submission (above) is send-only and talks directly to Proton's public smtp.protonmail.ch endpoint — no local software required, which is exactly why it's the one that fits a headless server/relay use case. Bridge is for a desktop mail client that needs full read/write access to a Proton mailbox; it's not a relayhost pattern at all, since it only listens on loopback on whatever machine it's running on.

Relaying via a generic smart host on a non-standard port

The same pieces recombine for any provider/port combination — for example, relaying through Gmail from a small device using port 587 explicitly, with a full recipient-restriction chain spelled out:

smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = raspberrypi.example.local
mydomain = example.local
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = $mydomain
mydestination = [email protected], raspberrypi, localhost.localdomain, localhost
relayhost = [smtp.gmail.com]:587
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = loopback-only
local_transport = error: local delivery disabled
smtp_use_tls = yes
smtp_sasl_auth_enable = yes
smtp_sasl_security_options =
smtp_sasl_password_maps = hash:/etc/postfix/sasl_password
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt

Checklist: relayhost setups

  • [ ] Is inet_interfaces set to loopback-only (unless this host also legitimately needs to receive mail)?
  • [ ] Is local_transport set to reject local delivery, if this is a pure null client?
  • [ ] Does the credentials file have 0600 permissions, and has the plaintext copy been removed after postmap generates the .db?
  • [ ] Does the relayhost's port match what the provider expects (587 for STARTTLS submission, sometimes 465 for implicit TLS, or a provider-specific plain port like Mailgun's smtp.mailgun.org default)?
  • [ ] Has SPF/DKIM been updated on the sending domain to authorize the relay provider (see the email-fundamentals authentication doc) — Postfix being configured correctly doesn't help if the receiving side's SPF check fails because the provider isn't listed?