Share Postfix Lookup Tables Safely with proxymap
An expensive lookup hit by every Postfix process at once is a classic way to grind mail delivery to a crawl, and proxymap is the fix. By the end, a lookup such as MySQL-backed virtual aliases runs through one controlled pool of proxymap processes, with the map explicitly allowed and the change verified. This guide targets Postfix 3.8.6 as installed on the reference system. Allow 10 to 20 minutes if the lookup table already exists; database credentials and map syntax are outside its scope.
The route
Jump straight to the step you need, or tick off Done means at the end.
What proxymap changes
proxymap(8) is a Postfix-managed lookup proxy. You do not start its binary yourself, and it never accepts mail from users. Postfix services ask it to open, read, or, through the separate proxywrite service, update an approved table.
The useful read-only case is sharing an expensive table between Postfix processes. This setting, for example, tells the virtual alias lookup to use a proxied MySQL map:
virtual_alias_maps = proxy:mysql:/etc/postfix/virtual_alias.cf
The proxy can also work around a chroot boundary, but that is not a reason to proxy every lookup you have. It is a shared daemon handling multiple clients, so keep high-latency tables away from it. The manpage also warns against using it for sensitive data such as UNIX IDs, mailbox paths or external commands.
1. Record the installed defaults
Run these ordinary, read-only commands as the Postfix administrator. They show the version, the configuration directory, and the two allow-lists that enforce the proxy boundary.
$ postconf mail_version config_directory data_directory max_idle max_use
mail_version = 3.8.6
config_directory = /etc/postfix
data_directory = /var/lib/postfix
max_idle = 100s
max_use = 100
$ postconf proxy_read_maps proxy_write_maps
max_idle is how long an idle server waits before leaving, and max_use is the maximum number of incoming connections one server handles before it does. The Postfix master creates more servers as needed, within the service process limit set in master.cf.
Checkpoint
Save the current output somewhere suitable for your change record. If the map is writable, also identify the Postfix-owned data directory before changing anything.
2. Add a read-only map
- Back it up first. This is an elevated command because
main.cfis normally root-owned:$ sudo cp --preserve=mode,ownership,timestamps /etc/postfix/main.cf /etc/postfix/main.cf.before-proxymap - Set the application parameter. Edit
/etc/postfix/main.cfand keep theproxy:prefix: drop it, and the client opens the table directly, withproxymapnever involved:virtual_alias_maps = proxy:mysql:/etc/postfix/virtual_alias.cf - Check the result. Confirm the expanded parameter includes the map family you just changed:
$ sudo postconf virtual_alias_maps proxy_read_maps virtual_alias_maps = proxy:mysql:/etc/postfix/virtual_alias.cf proxy_read_maps = ... $virtual_alias_maps ...
The exact default list varies with the package build. Replaced proxy_read_maps with a custom value? Include the relevant lookup parameter there, or proxymap will refuse the request with a DENY status.
3. Reload and exercise the real lookup
Reloading Postfix is an elevated, service-affecting action. It asks the master to reread configuration; it does not rewrite your map.
$ sudo postfix check
$ sudo postfix reload
postfix/postfix-script: refreshing the Postfix mail system
Use the normal application path that consumes the map, then check the mail log for a failed connection, permission error, missing map, or proxy denial. The manpage says problems and transactions get logged through syslogd or postlogd; the exact log file is distribution-specific.
$ sudo journalctl -u postfix --since "10 minutes ago" --no-pager
$ postconf -h virtual_alias_maps
A clean configuration check is not proof the database credentials or SQL query actually work. Verification needs a safe lookup through the Postfix feature that uses the table, with a test address or key that cannot redirect real mail.
4. Treat proxywrite as a separate risk
Read-only proxying uses proxymap. Updates and deletes use proxywrite, and only maps listed in proxy_write_maps are approved. Postfix 2.5 and later support these operations, and so does the installed 3.8.6.
Do not add a writable map just to make a failed read work. If an application genuinely needs updates, store the data under the Postfix-owned data_directory or another directory writable only by the mail system. Never put a Postfix-writable file in a root-owned directory with mismatched ownership. For a file-based map that cannot safely have multiple writers, the manpage requires a process limit of 1 on the proxywrite service in master.cf.
Warning
Read-write proxymap does not explicitly close tables between updates. Do not use it with formats that can end up persistently inconsistent between updates, such as CDB. Use a format with sync-on-update support, or a real database system, when consistency actually matters.
5. Recover from a bad change
If the lookup fails after the reload, stop testing against production traffic. Restore the saved configuration, validate it, and reload again:
$ sudo cp --preserve=mode,ownership,timestamps /etc/postfix/main.cf.before-proxymap /etc/postfix/main.cf
$ sudo postfix check
$ sudo postfix reload
$ postconf -h virtual_alias_maps
For a narrower rollback, remove only the proxy: prefix and restore the previously working map setting, if direct access is safe in your deployment. Do not delete a database or rebuild a map as a first response. Keep the log entry and compare the map path, permissions, allow-list, chroot visibility and backend credentials.
Done means
- The intended lookup parameter contains the documented
proxy:prefix. - The map is allowed by
proxy_read_maps, or byproxy_write_mapsfor an explicitly required update path. postfix checkpasses and the master has been reloaded.- A harmless real lookup succeeds and recent Postfix logs show no proxy denial or backend failure.
- Any writable data is owned and confined to the Postfix mail system, with a rollback copy of
main.cf.