Two major versions in one jump, which the documentation says not to do and which is what the situation actually was.
the route taken: rebuild, not upgrade.
1 a new host on 12, provisioned from the same scripts
2 the application deployed to it, in parallel
3 traffic moved with a DNS change and a low TTL
4 the old host kept for a fortnight
what this found: four things the provisioning scripts did
not do, all added by hand in 2020 and never written down.
a cron entry, a logrotate config, an ssl_dhparam file,
and a php.ini override for one setting.
A rebuild is the honest test of whether a host is reproducible, and finding four undocumented changes is a good outcome from a routine upgrade. The in-place path across two majors is possible and turns a scheduled task into an unbounded one; the rebuild has a rollback that is a DNS change. What it costs is the parallel running, which for a single application server is a fortnight of one extra machine.