Two dates are driving this work right now. Ubuntu 20.04 LTS left standard support on 31 May 2025 and is only receiving patches for machines attached to Ubuntu Pro's ESM. Ubuntu 22.04 LTS leaves standard support on 1 June 2027 — about nineteen months from today, which sounds like plenty until you price in the PHP work that comes with it. Ubuntu 24.04 LTS is supported until May 2029, and 26.04 LTS shipped in April 2026, with its .1 point release in August.
If your application server is on 20.04 or 22.04, the move is routine. It is also the single upgrade most likely to break a legacy PHP app in a way nobody predicted, because do-release-upgrade changes the OS, the web server, the database, the TLS library and the PHP interpreter in one unattended run. The fix is not to avoid the upgrade. It is to stop treating it as one change.
What each hop actually ships
The reason an OS upgrade feels like an application upgrade is that it is one. Rough shape of the distro packages:
| Ubuntu 20.04 | Ubuntu 22.04 | Ubuntu 24.04 | |
|---|---|---|---|
| PHP | 7.4 | 8.1 | 8.3 |
| MySQL | 8.0 | 8.0 | 8.0 |
| Apache | 2.4.41 | 2.4.52 | 2.4.58 |
| OpenSSL | 1.1.1 | 3.0 | 3.0 |
Verify these yourself on a scratch VM with apt policy php apache2 mysql-server rather than trusting a table in a blog post, including this one. Distro package versions move at point releases, and 26.04's PHP and MySQL defaults are newer again.
Read the table as a list of separate projects. A 20.04 box going to 24.04 is a PHP 7.4 to 8.3 migration wearing an OS upgrade as a disguise, and Ubuntu will not let you skip 22.04 on the way — do-release-upgrade goes LTS to LTS, so that is two reboots and two sets of config merges. A 22.04 box going to 24.04 is a PHP 8.1 to 8.3 migration, which is a much smaller job but still not a no-op.
Decouple the PHP jump from the OS jump
The single most useful thing you can do is make the interpreter version an independent variable. Add Ondřej Surý's PPA on the current OS, install the PHP version the next OS ships, and move the application onto it while everything else stays still:
add-apt-repository ppa:ondrej/php
apt update
apt install php8.3-fpm php8.3-mysql php8.3-mbstring php8.3-curl php8.3-xml php8.3-gd php8.3-intl php8.3-zip
Run the new FPM pool on a second socket, point one Apache vhost at it, and soak the application there. Our notes on the gotchas that actually bite and on building an upgrade inventory with PHPCompatibility and Rector cover the code side.
When the app is running clean on 8.3 on the old OS, the release upgrade becomes an OS upgrade only — and if it goes wrong, you know the interpreter is not the reason. That ordering is worth the extra week it costs.
In-place upgrade or new box?
Build a new server instead of upgrading in place when any of these is true:
- The box has been hand-edited for years and no configuration management describes it. An in-place upgrade preserves that history, including the parts nobody understands.
- You are two LTS releases behind. Two sequential
do-release-upgraderuns on a production host is more exposure than one fresh install plus a cutover. - The server is a snowflake with no rehearsal copy. If you cannot practise the upgrade, do not perform it on the only instance you have.
Upgrade in place when the host is managed by Ansible or similar, is one LTS behind, and you can rehearse on a restored snapshot. In-place is genuinely cheaper then: no DNS change, no data copy, no IP allowlists to update at third parties.
The new-box path also gives you something an in-place upgrade cannot: the old server, still running, for as long as you want it. That is the warm rollback.
Rehearse on a restored snapshot
Clone the production volume or restore last night's backup onto an identical instance, then run the real upgrade command against it:
apt update && apt full-upgrade
reboot
do-release-upgrade
Write down every prompt it asks. The config-file merge questions are the whole point of the rehearsal — do-release-upgrade stops and asks what to do about each modified file in /etc, and on a LAMP box that list usually includes apache2.conf, your vhosts, php.ini, the FPM pool config, my.cnf, and sshd_config. Diff them before answering:
apt install -y debsums
debsums -ce
Keep your versions of the application-affecting files; take the maintainer's version of anything you never deliberately changed. Decide this on the rehearsal host, not at 2 a.m. on production with a prompt blinking at you.
Also capture a before-and-after list so nothing silently disappears:
dpkg -l > /root/pkgs-before.txt
apachectl -M | sort > /root/apache-mods-before.txt
php -m | sort > /root/php-mods-before.txt
After the upgrade, diff all three. Disabled Apache modules and missing PHP extensions are the two most common post-upgrade surprises, and both are trivial to fix once you can see them.
The things that actually break
OpenSSL 1.1.1 to 3.0 (the 20.04 to 22.04 hop). The default security level rejects certificates and ciphers that used to work. If the application talks to an old payment gateway, a bank SFTP host, or an internal appliance with a SHA-1 or 1024-bit certificate, that connection fails after the upgrade, not during it. Inventory outbound TLS endpoints first and test each one from the rehearsal host. Our Apache and TLS hardening checklist has the server-side half.
MySQL's my.cnf merge. The package upgrade will offer you a new default config. Accepting it can silently drop your innodb_buffer_pool_size, which turns a tuned database into a default one and makes the app slow in a way that looks like "the new OS is slower". Keep your file, then reconcile removed options by hand.
Removed and renamed PHP extensions. php-json is built in since 8.0; php-mcrypt has been gone for years and anything still calling it needs openssl or sodium. Check php -m against your old list before you let traffic in.
Cron, systemd timers and PHP CLI. update-alternatives decides which binary php means. Cron jobs invoking a bare php can land on a different interpreter than your FPM pool uses. Pin the full path in crontabs: /usr/bin/php8.3.
Third-party apt repositories. do-release-upgrade disables them, upgrades, and leaves them disabled. Anything installed from a vendor repo — the PHP PPA, Percona tools, an agent, a monitoring collector — needs its sources rewritten for the new codename and re-enabled afterwards. List them first: grep -rh '^deb' /etc/apt/sources.list.d/.
Cutover and the hour after
For an in-place upgrade: snapshot the volume immediately before you start, and keep the snapshot until the app has been through a full business cycle — including the monthly batch jobs, which are the ones nobody tests. For a new-box cutover: lower the DNS TTL a day ahead, keep the old host running and reachable, and be ready to point back.
In the first hour, watch four things: the PHP-FPM error log, the Apache error log, MySQL's slow query log, and the queue of whatever background worker the app runs. Most post-upgrade failures are loud in one of those four within minutes. The quiet ones — a broken outbound TLS connection, a cron job that no longer runs — show up over the following week, which is why the snapshot stays.
If you are on 20.04 today
Attach the host to Ubuntu Pro now if it is not already. ESM for 20.04 runs to April 2030, and it costs nothing for a handful of machines. That buys you patched packages while you plan the real move — it does not buy you new PHP versions, and 20.04's PHP 7.4 has been out of upstream security support since November 2022. ESM is a bridge, not a destination.
Then do the work in the order above: PHP first on the old box, OS second, 24.04 as the target. 26.04 will still be there when its point releases have settled.
If you want the version-exposure picture across your whole estate before deciding what to touch, that is the first half of our LAMP health and security review. If you already know what you need and want someone to rehearse and run it, tell us the stack and versions and we will tell you honestly how big a job it is.