
Automate Postfix Transport Map Updates Without Downtime
Before you run this
This guide gives you a small bash script that rebuilds a Postfix transport map from its flat-file source, validates the configuration, and reloads Postfix so the new routing takes effect. postfix reload re-reads configuration and recycles daemons gracefully — it does not drop in-flight SMTP connections or stop the queue, which is why we use it instead of restart.
A few things to be clear about before you touch a live mail server:
- It needs root.
postmap,postfix check, andpostfix reloadall touch files under/etc/postfixand signal the master process. Run the script withsudoor as root. - It changes mail routing. The transport map decides where mail for a domain goes next. A wrong entry silently misroutes or defers mail for that domain — it does not usually bounce it immediately, so mistakes can go unnoticed for a while. Editing the map is not destructive to data, but it is production-affecting.
- Test the source file first. Read the script before running it. Try it against a copy of your transport file, or on a staging mail server, before pointing it at production. Use the built-in dry-run flag to see what would happen without reloading.
- Keep a backup. The script copies the current flat file and its compiled
.dbbefore rebuilding, so you can restore the previous routing in seconds. Do the first real run in a maintenance window if the domain you are changing carries live mail.
Assumed environment: Debian 12 / Ubuntu 22.04, Postfix 3.x, managed by systemd, with transport maps stored as a Berkeley DB hash — the Debian default. If you compiled Postfix with LMDB or use a different distribution, the map type prefix (hash: vs lmdb:) and the package paths differ; everything else holds.
How transport maps actually update
The flat file at /etc/postfix/transport is human-readable. Postfix does not read it directly — it reads the compiled index (transport.db) that postmap produces from it. So a change to routing is a two-step operation: edit the text file, then recompile the index.
A running trivial-rewrite daemon (the one that does transport lookups) holds the map open. The reliable, documented way to make every daemon pick up the rebuilt map immediately and deterministically is postfix reload. That is what the script does after a successful postmap.
First, confirm where your transport map lives and how it is referenced:
postconf transport_maps
You should see something like transport_maps = hash:/etc/postfix/transport. If it is empty, transport maps are not enabled yet — enabling them is a main.cf change outside the scope of this how-to; see the transport(5) man page and the Postfix documentation on transport_maps before adding it.
A transport file entry looks like this — the destination (domain or address on the left), the transport and nexthop on the right:
# /etc/postfix/transport
example.com smtp:[mail.internal.example.com]:25
.example.net relay:[10.0.0.25]
support@example.org local:
Replace those with your real domains and next-hops. The square brackets tell Postfix to skip MX lookup and go straight to that host.
The update script
Save this as /usr/local/sbin/update-transport.sh and make it executable with chmod 750. It edits nothing itself — you edit the flat file first, then run this to compile, validate, and reload.
#!/usr/bin/env bash
set -euo pipefail
# Path to the flat-file transport source. Change if yours differs.
MAP="/etc/postfix/transport"
BACKUP_DIR="/var/backups/postfix"
STAMP="$(date +%Y%m%d-%H%M%S)"
DRYRUN="${1:-}"
# Must run as root to postmap and reload.
if [[ "$(id -u)" -ne 0 ]]; then
echo "Run this as root (sudo)." >&2
exit 1
fi
# Confirm the source file exists before doing anything.
[[ -f "$MAP" ]] || { echo "Missing $MAP" >&2; exit 1; }
mkdir -p "$BACKUP_DIR"
# Back up the current flat file and compiled db so we can roll back.
cp -a "$MAP" "$BACKUP_DIR/transport.$STAMP"
[[ -f "$MAP.db" ]] && cp -a "$MAP.db" "$BACKUP_DIR/transport.db.$STAMP"
echo "Backed up to $BACKUP_DIR (stamp $STAMP)"
if [[ "$DRYRUN" == "--dry-run" ]]; then
# Show what postmap would build, without touching the live db.
echo "[dry-run] Would run: postmap $MAP"
echo "[dry-run] Would run: postfix check && postfix reload"
exit 0
fi
# Recompile the index from the flat file.
postmap "$MAP"
echo "Rebuilt $MAP.db"
# Validate configuration before signalling the master process.
postfix check
# Graceful reload: re-reads config, recycles daemons, keeps queue and
# in-flight connections alive. Not a restart.
postfix reload
echo "Postfix reloaded."
Run a dry run first:
sudo /usr/local/sbin/update-transport.sh --dry-run
Then the real thing:
sudo /usr/local/sbin/update-transport.sh
If you keep the transport file in Git or push it from Ansible, call this same script as the last step after the file lands. That gives you one consistent, backed-up, validated path for every change.
There are other ways to serve this data — proxymap to share one open map across daemons, or a socketmap/SQL/LDAP backend for large or dynamic routing. Those exist and are worth knowing by name, but for a file-based transport map the compile-and-reload flow above is the standard, boring, correct one.
Verify it worked
Two checks. First, query the compiled map directly and confirm the answer matches what you put in the flat file:
# Ask the compiled db how it would route this domain
postmap -q "example.com" hash:/etc/postfix/transport
That should print the transport and nexthop you configured. For a subdomain wildcard like .example.net, query an actual host under it.
Second, confirm Postfix reloaded cleanly and is serving mail. On systemd:
systemctl status postfix --no-pager
journalctl -u postfix --since "5 minutes ago" | tail -n 20
Look for a normal running state and a postfix/master ... reload log line, with no warnings about the transport map. If you want to watch a real message route, send a test to an address in the affected domain and follow it in /var/log/mail.log.
Undo
Because the script backed up both the flat file and the compiled .db before rebuilding, rolling back is a copy plus a reload. Pick the stamp you want from /var/backups/postfix:
# Restore the previous flat file and its compiled index
sudo cp -a /var/backups/postfix/transport.YYYYMMDD-HHMMSS /etc/postfix/transport
sudo cp -a /var/backups/postfix/transport.db.YYYYMMDD-HHMMSS /etc/postfix/transport.db
# Re-apply the restored routing
sudo postfix check && sudo postfix reload
Substitute the real timestamp for YYYYMMDD-HHMMSS. Verify again with postmap -q that the restored routing is what you expect, and check mail.log that mail for the domain is flowing where it should.
For the exact meaning of every transport field and edge cases like empty transports and nexthop syntax, read the transport(5) man page and the Postfix documentation on transport_maps and postmap — both are on the official Postfix site and in your installed man pages.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Alert on a Growing Postfix Mail Queue with a Cron Script
A mail queue that quietly grows is one of those failures you notice late — usually when a user asks why their mail from three hours ago hasn't arrived. A backlog can mean a dead relay host, a DNS problem, a downstream server rejecting everything, or an outbound spam run from a compromised account. This guide sets up a small cron job that counts the queue and emails you when it crosses a threshold, so you hear about it early.
Email a Daily Postfix Delivery Summary with Bash
This guide sets up a small bash script that reads yesterday's Postfix log, runs it through pflogsumm to build a delivery summary (messages received/delivered/deferred/ bounced, top senders, deferral reasons), and emails that summary to you once a day from cron.
Automate Dovecot Mailbox Quota Reports with a Shell Script
This guide builds a small shell script that runs doveadm quota get for every mailbox on a Dovecot server, writes a plain-text usage report to a file, and emails you a summary that flags anyone at or above a threshold (90% by default). It is a read-only reporting script. It queries quota figures that Dovecot already tracks; it does not create, resize, recalculate, or delete anything in a mailbox.
Automate a Weekly Patch-and-Report Routine for a Small Server Fleet
This sets up a weekly, unattended package upgrade on each server, then emails you a plain-text report of what was upgraded and whether a reboot is now pending. The upgrade step runs apt-get upgrade non-interactively, so it changes installed software on the host. Package upgrades are not cleanly reversible: apt has no "undo the last upgrade" button, so treat this with the same caution as any change to a running server.




