systemctl status postfix says the service failed, or, more confusingly, says it's active while nothing listens on port 25. The tempting move is to open main.cf and start commenting out lines until something works. Before you do that, read what Postfix said when it refused to start, because it almost always names the exact problem.
This matters at a support desk too. A lot of config damage after upgrades comes from well-meant guessing: someone removes a parameter that wasn't the problem, Postfix still won't start, and now there are two problems instead of one.
Why systemctl status tells you so little
systemctl status shows the unit state and only the last handful of log lines. When a daemon dies during startup, those lines are often generic, something like the control process exiting with an error code. That tells you something failed, not what.
There's a second trap on Debian and Ubuntu. There, postfix.service is usually a thin wrapper, and the real mail system runs as an instance unit called [email protected]. The wrapper can report active (exited) even when the instance behind it has failed. So if you only look at postfix.service, you might conclude Postfix is fine while nothing is actually accepting mail.
Start by listing every Postfix-related unit so you know which one actually failed:
BASH
# Show all Postfix units and their state
systemctl list-units --all 'postfix*'
# Read recent journal entries for all of them, this boot only
journalctl -u 'postfix*' -b --no-pager -n 100
# Narrow down to errors and fatals
journalctl -u 'postfix*' -b --no-pager | grep -Ei 'fatal|error|panic'journalctl -u returns almost nothing, Postfix may be logging under its own syslog identifiers rather than the unit name. Running journalctl -b --no-pager | grep postfix catches those.
Reading what the journal actually says
warning: mean Postfix noticed something odd but carried on. Lines tagged fatal: mean it stopped. When a service won't start, you're looking for the first fatal: line, and the warnings right before it often explain the context.
Illustrative output format (the parameter, map, and paths below are placeholders, and wording varies by version):
TEXT
postfix/postfix-script: warning: symlink leaves directory: /etc/postfix/./makedefs.out
postfix/postconf: warning: /etc/postfix/main.cf: unused parameter: example_old_param=yes
postfix/master: warning: using backwards-compatible default setting append_dot_mydomain=yes
postfix/postmap: fatal: unsupported dictionary type: mysql
postfix/postfix-script: fatal: the Postfix mail system is not running/var/log/mail.log simply doesn't exist, and all Postfix logging lives in the journal. A missing mail.log is not a sign that Postfix logged nothing.
Ruling out the red herrings
backwards-compatible default setting warning. Postfix has a compatibility_level parameter. When newer Postfix releases change a default, they keep the old behavior for configs whose compatibility_level is lower than the release that changed it, and they log a warning so you know. These warnings are informational. Raising compatibility_level to silence them changes real behavior, such as relay restrictions, so treat that as a separate, deliberate change rather than part of fixing startup.
The second is unused parameter warnings from postconf. These mean a parameter in main.cf isn't recognized by the installed version, usually because it was renamed or removed, or because it's a typo. An unused parameter is ignored, not fatal. It's still worth noting, because a removed parameter you relied on means its behavior silently went away.
The third is a port conflict. If the journal shows a bind error on port 25, the fix is not in main.cf at all. Something else, often a second MTA pulled in as a dependency, is already listening. ss -ltnp shows which process owns the port.
The causes that actually stop Postfix after an upgrade
postfix package. A release upgrade can drop or fail to carry over one of those packages, and any main.cf line referencing mysql:/etc/postfix/... or pcre:/etc/postfix/... then becomes fatal. The fix is reinstalling the matching package, not rewriting the map. postconf -m lists the map types the installed build currently supports, which confirms this quickly.
Stale compiled maps are another. Files like /etc/aliases.db or /etc/postfix/transport.db are generated from plain-text sources. If the upgrade changed the underlying database library or its format, Postfix may fail to open the old .db files and log an open or format error naming the file. Rebuilding them from source with postmap or newaliases is the usual fix. This rewrites only the generated file, but check that the plain-text source is still present and correct before you run it.
Removed or renamed parameters can turn fatal when they're used in a way Postfix now rejects, especially in TLS settings. A newer OpenSSL can refuse old certificate keys, dropped protocol names, or cipher settings that used to work. The error usually mentions TLS, a certificate path, or a cipher setting directly.
Finally, config merges. During the upgrade, the package manager may have asked whether to keep your main.cf or master.cf or take the maintainer's version. Keeping your old master.cf while daemon behavior changed, or accepting the new one and losing your customizations, both show up here. Look for .dpkg-dist, .dpkg-old, or .rpmnew files in /etc/postfix and diff them against what's live.
Checking the config with postfix check and postconf
BASH
# Validate config, file ownership and permissions. Silence means no problems found.
postfix check
# Show only settings that differ from defaults, plus unused parameter warnings
postconf -n
# Installed version and current compatibility level
postconf mail_version compatibility_level
# Map types this build supports
postconf -m
# Leftover package config files to compare against
ls -l /etc/postfix/ | grep -Ei 'dpkg|rpmnew|rpmsave'postfix check is the most useful one here. It runs the same sanity checks the startup script does, so if it prints a fatal, that's the same error that stopped the service, but now you can reproduce it without restarting anything. postconf -n gives you a short list of everything you've customized, which is a better starting point for review than scrolling through the whole main.cf.
Edit only what the error points to. If the fatal names a map type, fix the package. If it names a file, fix that file. If it names a parameter, look that parameter up in the man page for the installed version (man 5 postconf) before changing it, since the replacement may behave differently.
Verifying the fix
- Run
postfix checkagain and confirm it prints nothing. - Restart with
systemctl restart postfix, then runsystemctl list-units --all 'postfix*'and confirm the instance unit, not only the wrapper, is active. - Run
postfix statusto confirm the master process is running. - Run
ss -ltnpand confirm the expected process is listening on port 25, and on 587 or 465 if you use submission. - Follow the log live with
journalctl -u 'postfix*' -f, send a test message out and one in, and watch for fresh warnings or deferrals.
warning: lines appear, note them rather than ignoring them. They're often the backwards-compatibility messages that tell you which defaults will change if you ever raise compatibility_level.
The practical habit is simple: when Postfix won't start after an upgrade, the first command is journalctl, the second is postfix check, and the editor comes third. The fatal line almost always tells you whether you're dealing with a missing package, a stale map file, a removed parameter, or a config merge, and each of those has a narrow fix. Changing only what the log points to keeps a one-problem outage from turning into a config you no longer trust.