Ubuntu 22.04 to 26.04 LTS Upgrade: Gotchas & Fixes

Share this:

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

  1. Audit what’s actually running on the server (services, ports, disk, PHP modules)
  2. Take backups (database, Apache config, webroots, certs) on top of an existing full server snapshot
  3. Pre-flight checks before each hop: clear package holds, clear pending reboots, confirm the upgrade path
  4. Hop 1: 22.04 to 24.04
  5. Fix what broke on hop 1
  6. Pre-flight checks again
  7. Hop 2: 24.04 to 26.04
  8. Fix what broke on hop 2
  9. 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-upgrade silently disabled my main/universe/multiverse sources and failed with a misleading ubuntu-minimal not downloadable error. Had to point sources at archive.ubuntu.com before 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.sock after 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-resolved had no upstream server to fall back on. Fixed with a static /etc/resolv.conf.
  • do-release-upgrade refused 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 -d flag to force it.
  • MySQL authentication broke completely after 8.0 to 8.4: MySQL 8.4 disables the mysql_native_password plugin 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 in mysqld.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 -m after each hop and diffed it against the original module list to see what dropped off. intl and memcached had 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.

Linode Cloud Manager Backups tab showing a pending Stable 22.04 LTS snapshot

Initial audit

lsb_release -a
uname -a
systemctl list-units --type=service --state=running
sudo ss -tulpn
df -h
Terminal output of lsb_release -a showing Ubuntu 22.04.5 LTS Jammy Jellyfish

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 &amp;&amp; sudo apt full-upgrade -y
sudo apt install update-manager-core -y
cat /etc/update-manager/release-upgrades   # confirm Prompt=lts
Terminal output of apt full-upgrade and release-upgrades config confirming Prompt=lts

Then check for a pending reboot. do-release-upgrade refuses to run otherwise:

ls /var/run/reboot-required 2&gt;/dev/null &amp;&amp; echo "Reboot needed" || echo "No pending reboot"
Terminal output confirming no pending reboot before running do-release-upgrade

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
Terminal output of sources.list mirror swap to archive.ubuntu.com and do-release-upgrade starting successfully

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:

do-release-upgrade summary screen showing 80 packages removed, 188 new, 775 upgraded

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).

dpkg configuration file conflict prompt for apache2.conf, keeping the locally modified version
dpkg configuration file conflict prompt for sshd_config, keeping the locally modified version

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.

dpkg notice that resolvconf was removed and a reboot is recommended

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

do-release-upgrade completion message stating the system upgrade is complete and a restart is required

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 &gt; /dev/null &lt;&lt;'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:

Terminal output of lsb_release -a showing Ubuntu 24.04.4 LTS Noble Numbat
Terminal output confirming /etc/resolv.conf is a static file with working nameservers

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).

Terminal output of apt full-upgrade, package hold check, and reboot check before hop 2
Terminal output of sources.list mirror swap to archive.ubuntu.com ahead of hop 2

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
Terminal output of do-release-upgrade -d welcoming Ubuntu 26.04 LTS Resolute Raccoon

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.

do-release-upgrade notice listing unofficial packages installed from Ubuntu ESM Apps
dpkg popularity-contest configuration prompt with No selected

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/
Terminal output of installing php8.5-memcached after the hop 2 upgrade
Terminal output removing stale PHP 7.x Apache conf files and confirming a 200 OK response from the site

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 intl going missing via a WordPress Site Health warning, then confirmed with php -m | grep -i <module>. Both were a straightforward reinstall (sudo apt install php8.5-memcached php8.5-intl, then sudo 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.
Terminal output of php -m listing installed PHP modules after reinstalling missing extensions
Terminal output of memcached stats showing hit count increasing before and after a page load

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
Terminal output of apt autoremove clearing obsolete packages after the upgrade
Terminal output of switching sources.list back to the Linode mirror for faster day-to-day updates

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 🙂
Buy Me A Coffee
Share this:

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.