Postfix isn't one program. It's a set of small, single-purpose daemons supervised by a master process. They talk to each other over local sockets, and each does one part of the mail-handling pipeline with the least privilege it can. Understanding this shape makes the rest of the configuration, and every log line, much easier to reason about.
The daemon pipeline¶
network (25/587/465) local submission (sendmail, mail, cron)
│ │
┌──────▼───────┐ ┌──────▼──────┐
│ postscreen │ (optional) │ postdrop → │
│ → smtpd │ │ maildrop/ │
└──────┬───────┘ │ → pickup │
│ └──────┬──────┘
└───────────────┬──────────────────────┘
┌──────▼──────┐ trivial-rewrite (address
│ cleanup │◄─ rewriting & routing lookups)
│ header/body │
│ checks, │── milters (DKIM, rspamd…)
│ canonical/ │
│ virtual │
└──────┬──────┘
▼ incoming/ → active/
┌─────────────┐
│ qmgr │ queue manager: schedules delivery,
│ │ retries deferred mail
└──────┬──────┘
┌─────────┬───────┼─────────┬──────────┐
▼ ▼ ▼ ▼ ▼
smtp(8) lmtp(8) local(8) virtual(8) pipe(8)
(internet / (Dovecot) (system (virtual (scripts:
relayhost) users) mailboxes) autoreply…)
│
bounce / defer / trace → DSNs back to the sender
| Daemon | Job |
|---|---|
master |
The supervisor. Starts the others on demand per master.cf and enforces process limits |
postscreen |
Optional front door on port 25: cheaply filters bots (DNSBL scoring, pregreet and protocol tests) before handing clean connections to smtpd |
smtpd |
Speaks SMTP to incoming connections and enforces smtpd_*_restrictions |
pickup |
Collects locally submitted mail from maildrop/ |
cleanup |
Normalizes the message, adds missing headers, applies header_checks/body_checks, canonical and virtual alias rewriting, and milters, then writes it to the queue |
trivial-rewrite |
Address rewriting and deciding which transport and next hop a recipient uses |
qmgr |
The scheduler. Moves messages into active/, picks a delivery agent, and handles retries and backoff |
smtp / lmtp |
Outbound delivery over SMTP (internet or relayhost) or LMTP (to Dovecot) |
local / virtual |
Built-in delivery to system users / virtual mailboxes |
pipe |
Hands a message to an external program |
bounce, defer, trace |
Generate delivery status notifications |
anvil |
Connection and rate counters used for rate limiting |
tlsmgr, scache, proxymap |
TLS session cache and randomness, connection cache, shared lookup-table connections |
The queue directories¶
Every message exists as a file under /var/spool/postfix/ (queue_directory) at some point, moving between subdirectories as its status changes:
| Directory | Meaning |
|---|---|
maildrop |
Locally submitted mail waiting for pickup |
incoming |
Being written or just written by cleanup |
active |
Messages qmgr is currently delivering (kept small and in memory) |
deferred |
Temporary failure; waiting to retry with exponential backoff |
hold |
Held indefinitely by postsuper -h or a HOLD action; nothing happens until released |
corrupt |
Unreadable queue files |
bounce, defer, trace |
Per-message delivery status logs used to build DSNs |
Never edit or move these files by hand. Use postqueue and postsuper (see Administration and Queue Management).
The two configuration files¶
main.cfholds global parameters (name = value): identity, networks, restrictions, lookup tables, TLS. Almost everything in these guides lives here.master.cfholds one line per service: which daemons listen where (smtp inet,submission inet,smtps inet), process limits, chroot, and per-service-oparameter overrides. You touch it to enable submission on 587/465, add apipeservice, or add a content filter.
Use postconf rather than an editor for single changes. It validates parameter names:
postconf -n # only settings that differ from defaults
postconf -d mydomain # the compiled-in default
postconf mydestination # current effective value
postconf -e 'myhostname = mail.domain.tld' # set a value (edits main.cf)
postconf -M # master.cf services
postconf -P # master.cf -o overrides
postfix check # sanity-check config and file permissions
postfix reload # apply main.cf/master.cf changes without dropping connections
compatibility_level in main.cf controls which generation of safer defaults applies. A fresh install sets it to the current version. When upgrading old configs, Postfix logs which legacy default it's still using. Read those warnings, then raise the level.
Lookup tables¶
A large number of settings, such as virtual_alias_maps, alias_maps, smtp_sasl_password_maps, transport_maps and virtual_mailbox_maps, take a lookup table reference in the form type:name:
| Type | What it is |
|---|---|
hash: / btree: |
Berkeley DB files compiled from a text source with postmap (or postalias for aliases) |
lmdb: |
LMDB files compiled the same way. The replacement for hash:/btree: |
cdb: |
Constant DB, very fast read-only files. Also a hash: replacement for static maps |
texthash: |
Reads the text file directly at startup; no compile step. Good for small maps, but needs postfix reload after edits |
regexp: / pcre: |
Pattern tables (header checks, address rewriting). Not compiled |
inline:{ key=value, … } |
A tiny table written directly in main.cf |
static:value |
Always returns the same value |
mysql:, pgsql:, ldap:, sqlite: |
Live queries defined in a small .cf file |
proxy:<type>:… |
Route lookups through proxymap so all processes share pooled connections (and chrooted daemons can reach the files) |
unionmap:{…} / pipemap:{…} |
Combine tables (first match wins / chain results) |
The compile step is the single most common "why isn't my change taking effect?" mistake. For any compiled table type, edit the source, then:
postmap lmdb:/etc/postfix/virtual # or plain `postmap /etc/postfix/virtual` for the default type
postalias /etc/aliases
postfix reload # needed for some maps; harmless otherwise
If you forget, Postfix keeps using the old .db/.lmdb and logs database … is older than source file ….
Berkeley DB is going away¶
Several distributions are removing Berkeley DB from their Postfix builds, which removes hash: and btree:. Check what your system defaults to and migrate before it's forced on you:
postconf default_database_type # hash, lmdb or cdb
postconf -m # which table types this build supports
Migration is mechanical: change hash: to lmdb: (or cdb:) in main.cf, re-run postmap/postalias on each source file, and postfix reload. Postfix's NON_BERKELEYDB_README covers the details, including a compatibility setting that silently redirects hash: lookups during the transition. Examples in these guides use hash: for familiarity. Substitute your default_database_type.
Core identity settings¶
| Setting | Meaning |
|---|---|
myhostname |
This machine's FQDN, used in the EHLO greeting and Received: headers. It should match the sending IP's PTR record |
mydomain |
The parent domain, derived from myhostname by default |
myorigin |
The domain appended to unqualified addresses from this host (what turns root into [email protected]) |
mydestination |
Domains delivered locally to system accounts |
mynetworks |
Client IP ranges trusted to relay without authentication. Keep it minimal |
inet_interfaces |
Interfaces Postfix listens on: all, loopback-only, or specific addresses |
inet_protocols |
ipv4, ipv6 or all. Set ipv4 on hosts without working IPv6 to avoid timeouts |
relay_domains |
Domains this host accepts and forwards elsewhere (e.g. as a gateway for an internal server) |
relayhost |
Send all non-local mail through this smart host instead of using MX lookups |
Every other guide in this section is a combination or extension of these fundamentals for a specific goal: relaying outbound, hosting many domains, encrypting sessions, and so on.