Ubuntu 22.04 to 26.04 LTS Upgrade: Gotchas & Fixes
This is my walkthrough for upgrading Ubuntu 22.04 to 26.04 LTS, on a live production server. I’ve done this jump before, from 20.04 to 22.04 LTS, and the overall process is similar. This one just had more teeth to it.
Why document this? It’s so the next person doing this same jump, especially anyone else stuck on a Linode-style regional mirror, doesn’t have to rediscover it all from scratch.
High-level steps
- Audit what’s actually running on the server (services, ports, disk, PHP modules)
- Take backups (database, Apache config, webroots, certs) on top of an existing full server snapshot
- Pre-flight checks before each hop: clear package holds, clear pending reboots, confirm the upgrade path
- Hop 1: 22.04 to 24.04
- Fix what broke on hop 1
- Pre-flight checks again
- Hop 2: 24.04 to 26.04
- Fix what broke on hop 2
- Clean up: remove obsolete packages, revert temporary config changes, verify everything end to end
My server stack
Before touching anything, I audited what was actually running:
- Apache2 as the web server, fronting everything
- PHP 8.1-FPM
- MySQL 8.0
- memcached
- phpMyAdmin, on its own subdomain vhost
- Three custom Go services running behind Apache’s
mod_proxy(what they actually do isn’t relevant here)
Issues I ran into, summarised
Jump to the full turn-by-turn detail for any of these in the sections below.
- Mirror not recognised by the upgrade tool: Linode’s regional Ubuntu mirror isn’t on Canonical’s list of “known” mirrors, so
do-release-upgradesilently disabled mymain/universe/multiversesources and failed with a misleadingubuntu-minimal not downloadableerror. Had to point sources atarchive.ubuntu.combefore running the upgrade, on both hops. - Site went down after hop 1, stale PHP-FPM socket path: my vhost was still pointing at
php8.1-fpm.sockafter PHP moved to 8.3. Every vhost that touches PHP hardcodes its own socket path independently, so this bit me twice (main site on hop 1, phpMyAdmin on hop 2). - One of my Go services lost DNS resolution: it needs external web access, and
resolvconf‘s removal in 24.04 broke the DNS pipeline on my static-IP,ifupdown-managed box.systemd-resolvedhad no upstream server to fall back on. Fixed with a static/etc/resolv.conf. do-release-upgraderefused to see 26.04 as available: even after 26.04.1’s official release date, the “safe to upgrade” metadata hadn’t fully propagated. Needed the-dflag to force it.- MySQL authentication broke completely after 8.0 to 8.4: MySQL 8.4 disables the
mysql_native_passwordplugin by default, so any DB account created under it fails to authenticate at all, not just with a permissions error. Fixed by re-enabling the plugin inmysqld.cnf. - phpMyAdmin down, same socket issue as the main site: its vhost still pointed at the old PHP-FPM socket path. Fixed with the same socket-path correction as the main site.
- Missing PHP extensions after each version bump: ran
php -mafter each hop and diffed it against the original module list to see what dropped off.intlandmemcachedhad simply not carried over to the new PHP version and just needed reinstalling (sudo apt install php8.5-<module>, then restart FPM). I checked my active plugins for any dependency on either before deciding it was safe to leave them out.
Backup!
Definitely do a full server backup. I did mine using Linode’s snapshot feature.

Initial audit
lsb_release -a uname -a systemctl list-units --type=service --state=running sudo ss -tulpn df -h

systemctl list-units --type=service --state=running lists every systemd service currently active on the box, service name, state, and a short description pulled from that service’s own unit file, so you can see at a glance what’s actually running.
sudo ss -tulpn shows every TCP and UDP socket the machine is listening on (-t/-u), limited to listening sockets only (-l), with the owning process and PID attached (-p) and everything shown numerically rather than resolved to hostnames (-n), so you can see exactly what’s bound to which port and by what.
And with that, you now have an inventory of what’s running on your server, in case you forget any service that is running. This is also how I checked and prepare for any gotcha’s when upgrading to 26.04.
The upgrade path itself isn’t a straight line, either. You cannot jump 22.04 straight to 26.04. do-release-upgrade only ever offers the next LTS, so the real path is 22.04 to 24.04 to 26.04, with a reboot between each hop.
Hop 1: 22.04 to 24.04
Mirror not recognised by the upgrade tool
Pre-flight is straightforward:
sudo apt update && sudo apt full-upgrade -y sudo apt install update-manager-core -y cat /etc/update-manager/release-upgrades # confirm Prompt=lts

Then check for a pending reboot. do-release-upgrade refuses to run otherwise:
ls /var/run/reboot-required 2>/dev/null && echo "Reboot needed" || echo "No pending reboot"

Running sudo do-release-upgrade failed almost immediately with:
ERROR: 'ubuntu-minimal' was not downloadable
This sent me down a rabbit hole of checking apt update output, third-party PPAs, and Ubuntu Pro/ESM repos, all of which turned out fine. The actual cause, buried in /var/log/dist-upgrade/main.log:
entry 'deb http://mirrors.linode.com/ubuntu/ jammy main restricted' was disabled (unknown mirror)
do-release-upgrade checks your sources against Canonical’s list of known official mirrors. Linode’s regional mirror isn’t on it, so the tool silently disabled every main/universe/multiverse line pointing at it, leaving only security.ubuntu.com active. ubuntu-minimal lives in main. That’s the actual failure, and it comes with a completely misleading initial error message.
Fix: point sources.list at the official archive before running the upgrade tool.
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak-pre-upgrade sudo sed -i 's|mirrors.linode.com/ubuntu|archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list sudo apt update sudo do-release-upgrade

If you’re on any regional or provider mirror (Linode, DigitalOcean, Vultr, and so on), check this before you even attempt the upgrade. It’ll save you the detour.
With that fixed, the actual package summary showed up as expected:

Config conflicts, and the one to be careful with
Through the actual install, do-release-upgrade prompts on modified config files. General rule I followed: keep your existing version for anything under /etc/apache2/, /etc/php/, /etc/mysql/, or /etc/ssh/. The defaults aren’t wrong, but they’ll clobber your customisations (custom SSH port, vhost configs, and so on).


Partway through, a fallback SSH instance gets offered on a temp port (usually 1022), in case your main SSH breaks. If you’re behind a cloud firewall (Linode Cloud Firewall, AWS security groups, and so on) rather than relying purely on ufw, remember to allow that temp port there too. ufw being active doesn’t matter if the host-level firewall is what’s actually gatekeeping.
Also flagged, harmlessly: 24.04 drops resolvconf in favour of systemd-resolved managing DNS directly.

That notice turned out to matter more than it looked. Eventually:

After hop 1: two things broke
1. The site was down. Apache and MySQL were both running fine. The actual cause was that my vhost’s PHP handler still pointed at the old socket:
SetHandler "proxy:unix:/var/run/php/php8.1-fpm.sock|fcgi://localhost/"
PHP had moved to 8.3, php8.1-fpm was masked, and Apache was proxying requests into a socket that no longer existed.
sudo sed -i 's/php8.1-fpm.sock/php8.3-fpm.sock/g' /etc/apache2/sites-available/<sitename>-ssl.conf sudo systemctl reload apache2
Lesson for later: every vhost that touches PHP hardcodes its own socket path independently. There’s no central place this lives. I ended up hitting the exact same issue again for phpMyAdmin’s vhost on hop 2. After any PHP version bump, it’s worth grepping across the board instead of waiting for each site to break one at a time:
sudo grep -rl "fpm.sock" /etc/apache2/sites-available/
2. One of my Go services stopped fetching data. It needs external web access, and started throwing:
Error during page load: page load error net::ERR_NAME_NOT_RESOLVED
This traced back to the resolvconf removal notice from earlier. My server uses a static IP via the old ifupdown-style /etc/network/interfaces config, not netplan or DHCP. DNS had previously been supplied through a dns-nameservers line that resolvconf translated into /etc/resolv.conf. With resolvconf gone, that pipeline just stopped, and systemd-resolved had no upstream DNS server to fall back on (confirmed via resolvectl status showing empty scopes on every interface).
Fix, given this box’s static-IP setup: skip the dynamic resolver entirely and write a static resolv.conf.
sudo rm -f /etc/resolv.conf sudo tee /etc/resolv.conf > /dev/null <<'EOF' nameserver 8.8.8.8 nameserver 1.1.1.1 EOF
Confirmed as a regular file, not a symlink back to the resolved stub, so it survives reboots without depending on resolvconf, systemd-networkd, or DHCP. None of those are actually managing this box’s network:


If your server uses a static IP via the classic /etc/network/interfaces (common on older Linode or DigitalOcean images), watch for this specifically. It won’t show up on a DHCP-managed box.
Hop 2: 24.04 to 26.04
Same pre-flight pattern: full-upgrade, check for holds, check for a pending reboot, re-apply the mirror workaround (still needed, since Linode’s mirror still isn’t “known” to the tool, and this check happens fresh on every hop).


do-release-upgrade refused to see 26.04 as available
There is no development version of an LTS available.
This was despite 26.04.1 (the point release do-release-upgrade waits for) having already shipped. Canonical’s “this LTS is a safe upgrade target” metadata apparently hadn’t fully propagated yet. The -d flag forced it through:
sudo do-release-upgrade -d

A new prompt: unofficial packages
The following unofficial packages are currently installed:
imagemagick, libde265-0, libopenexr-3-1-30, php-twig, python3-pip
Installed from: UbuntuESMApps
These came from Ubuntu Pro’s ESM Apps repo, which is entitled but disabled on this account’s free personal Pro subscription. Since none of them are actively depended on for ESM’s extended patching, I let the upgrade continue and take the standard archive versions instead.


After hop 2: three more things to fix
1. PHP-FPM socket, again. This time 8.3 to 8.5. Same drill, different version number. Fixed it before rebooting this time, since the package swap (and the breakage) happens during install, not at reboot.
2. MySQL 8.0 to 8.4 broke authentication entirely:
ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded
MySQL 8.4 disables the mysql_native_password plugin by default. If your DB accounts were created years ago under that plugin (very likely for anything set up pre-2023-ish), they simply can’t authenticate anymore. The plugin isn’t loaded server-side at all, so even accounts with correct passwords fail the handshake.
The actual fix was small:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
Then add mysql_native_password=ON, under [mysqld]
sudo systemctl restart mysql
That alone brought WordPress’s DB connection back, since its account plugin loaded again. The proper long-term fix is migrating accounts to caching_sha2_password via ALTER USER ... IDENTIFIED WITH caching_sha2_password BY '...'. I’ve left that as a follow-up task rather than doing it under pressure with the site down.
3. phpMyAdmin. Its vhost still pointed at the old PHP-FPM socket path. I should have fixed this during hop 1 to 24.04 LTS. But I only remembered it after starting the upgrade to 26.04.1. Fixed with the same socket-path correction as the main site:
sudo sed -i 's/php8.1-fpm.sock/php8.5-fpm.sock/g' /etc/apache2/sites-available/phpmyadmin-le-ssl.conf sudo systemctl reload apache2
Worth doing this check across every vhost at once rather than one at a time:
sudo grep -rl "fpm.sock" /etc/apache2/sites-available/


What survived the jump cleanly
Not everything broke. Worth naming what didn’t, for balance:
- memcached and intl: both simply hadn’t carried over to PHP 8.5. Caught
intlgoing missing via a WordPress Site Health warning, then confirmed withphp -m | grep -i <module>. Both were a straightforward reinstall (sudo apt install php8.5-memcached php8.5-intl, thensudo systemctl restart php8.5-fpm), and once done, hit counts confirmed memcached working immediately. - mod_proxy / mod_proxy_fcgi for the Go services: survived both hops untouched.
- The Go service that needs external web access: once DNS was fixed on hop 1, it kept working straight through hop 2 with no further changes.


Two PHP extensions were genuinely gone, not just missing packages: comparing the PHP module list before and after, imap and xmlrpc were both absent. Unlike intl or memcached, these aren’t reinstallable oversights. PHP formally removed the imap extension as of 8.4 (deprecated since 8.1), and xmlrpc has been community-maintained only since PHP 8.0, with no guaranteed package for newer versions. I checked my active plugins for any dependency on either (grep -ril "imap_\|xmlrpc" wp-content/plugins/) and found nothing, so this was a non-issue here, but it’s worth checking explicitly rather than assuming.
Cleanup
sudo apt autoremove -y sudo apt autoclean sudo sed -i 's|archive.ubuntu.com/ubuntu|mirrors.linode.com/ubuntu|g' /etc/apt/sources.list sudo apt update


Also worth doing: removing the temporary SSH fallback port from your firewall once you’ve confirmed the primary SSH port is stable, and pulling any leftover .distUpgrade backup files from /etc/apt/sources.list.d/ if you don’t need them.
Two hops, two mirror workarounds, three broken services, and a fair bit of grep. The site’s fully on 26.04 now, and everything I’ve checked so far, Apache, MySQL, PHP-FPM, phpMyAdmin, memcached, and all three Go services, is confirmed working.
With this guide, I hope your upgrade process would be a lot smoother too. If you’re coming from 18.04 or 20.04, start with my 20.04 to 22.04 upgrade post first, then come back here for the rest of the road to 26.04.
If this post has been useful, support me by buying me a latte or two 🙂
