How To

Migrate Ubuntu 22.04 LTS to Ubuntu 26.04 LTS (Skip-Release Upgrade)

Ubuntu’s release upgrade tool does not let you jump straight from 22.04 to 26.04. The supported path is two hops, 22.04 → 24.04 → 26.04, with a reboot between each one. It works, it keeps your data, and when it goes sideways the failures are predictable enough to fix in the same session.

Original content from computingforgeeks.com - post 166909

This guide walks through the full skip-release upgrade on a real server, not an empty cloud image. The test VM runs a LAMP stack with a PHP dashboard and a MariaDB inventory database holding 500 customers, 120 products, and 2000 orders. Every step below was executed on that workload and the data is re-validated after each hop, with fastfetch output captured on every release so you can compare kernel, shell, OpenSSH, and PHP versions side by side.

Verified working: April 2026 on a Proxmox KVM guest, upgrade path 22.04.5 (kernel 5.15.0-173) → 24.04.4 (kernel 6.8.0-110) → 26.04 “Resolute Raccoon” development branch (kernel 7.0.0-14)

Why two hops, not one

The do-release-upgrade helper reads /etc/update-manager/release-upgrades to decide what release to offer, and on an LTS install it only offers the next LTS. From 22.04 that is 24.04. Ubuntu does not publish a skip-release upgrade metadata file, so there is no supported “22.04 to 26.04” path.

The path that works is sequential. Upgrade 22.04 to 24.04, reboot, confirm your workload, upgrade 24.04 to 26.04, reboot, confirm again. The whole thing took about 55 minutes on the test VM over a gigabit link, of which around 10 minutes was package download and the rest was unpack and post-configure.

Two things to know before starting. First, at the time of writing, 26.04 is still pre-release (GA is April 23 2026), so the second hop needs do-release-upgrade -d to see it. Once 26.04.1 ships around August 6 2026, the -d flag is no longer needed. Second, if you are coming from 24.04 already, skip straight to the second hop.

Test workload used in this guide

An empty server upgrade tells you the upgrade tool works. A server with a real database, a real PHP frontend and real connection pools tells you whether the apps survive. The VM in this walkthrough runs:

  • Apache 2.4.52 serving a PHP dashboard at / and a JSON API at /api/stats.php
  • MariaDB 10.6.23 with a populated inventory database (three tables, foreign keys, indexes)
  • 500 customer rows, 120 product rows, 2000 order rows.

After each upgrade the same curl http://localhost/api/stats.php is re-run. Row counts and the cancelled-order revenue sum must match to the cent, or the upgrade is a regression.

Step 1: Set reusable shell variables

Every command below uses shell variables for the backup path and admin email so you can copy the commands as-is. Export these at the top of your SSH session:

export BACKUP_DIR="/root/upgrade-backups"
export DB_NAME="inventory"
export ADMIN_EMAIL="[email protected]"
mkdir -p "${BACKUP_DIR}"
echo "Backups: ${BACKUP_DIR}"
echo "Database: ${DB_NAME}"

Replace DB_NAME with your own database name if you are following along with a different workload.

Step 2: Baseline the 22.04 host

Install fastfetch so you have a clean before-and-after view of kernel, shell, OpenSSH and PHP. It is not in the default 22.04 repos, grab the latest release from GitHub:

FF_URL=$(curl -sL https://api.github.com/repos/fastfetch-cli/fastfetch/releases/latest \
  | grep -oE 'https://[^"]+fastfetch-linux-amd64.deb' | head -1)
wget -q "${FF_URL}" -O /tmp/fastfetch.deb
sudo dpkg -i /tmp/fastfetch.deb
fastfetch --logo none

The output below is the baseline captured on the test VM. Note the kernel (5.15), shell (bash 5.1), OpenSSH (8.9p1) and PHP 8.1 signatures, those change on every hop.

fastfetch output on Ubuntu 22.04 LTS before the upgrade showing kernel 5.15, bash 5.1 and OpenSSH 8.9p1

With the baseline captured, plan the backups that will let you roll back if the upgrade damages state or configuration.

Step 3: Back up the workload before the first hop

The single most important thing to do before a distro upgrade is to inventory your workload and back up each piece that holds state or carries custom configuration. The commands below reflect the LAMP workload used in this guide. Your stack is almost certainly different, so treat them as a template, not a recipe.

Sit with pen and paper (or a short runbook) and list every piece of state before running anything. A practical checklist for most servers:

  • Databases. Dump every running DB engine, not just the one you think the app uses. MariaDB/MySQL via mariadb-dump or mysqldump --single-transaction, PostgreSQL via pg_dumpall, MongoDB via mongodump, Redis via redis-cli --rdb, SQLite by copying the .db file with the service stopped.
  • Application code and assets. Document roots, upload directories, user-generated content. For web apps that is typically /var/www, /srv, or a custom path. Many apps keep uploads outside the code directory, check.
  • Configuration. /etc in full is the safest, but at minimum capture the service directories you care about (/etc/apache2, /etc/nginx, /etc/php, /etc/postgresql, /etc/mysql, /etc/letsencrypt, /etc/ssh, /etc/systemd/system, /etc/fstab, /etc/netplan).
  • Secrets and credentials. Environment files, .env, cron-user crontabs (crontab -l -u <user>), API key files outside /etc. These are usually missed by generic /etc tarballs.
  • Container and orchestrator state. If you run Docker, back up docker-compose.yml files, named volume contents, and any bind-mounted host paths. For Kubernetes on the host, etcd and /var/lib/kubelet. Snap data lives under /var/snap/<snap>/common and /var/snap/<snap>/current.
  • Queues and background state. RabbitMQ definitions export, Celery beat schedules, systemd timer units, message broker journals.
  • User data and logs worth keeping. /home, custom /opt installations, the most recent weeks of /var/log for forensic reference.
  • The package manifest itself. Save dpkg --get-selections > "${BACKUP_DIR}/dpkg-selections.txt" and apt-mark showmanual > "${BACKUP_DIR}/apt-manual.txt" so you can tell the difference between what was installed before the upgrade and what survived it.

Copy the outputs off the host. A backup that lives on the same disk as the broken upgrade is not a backup. Push the tarballs to S3/Spaces/B2, an NFS share, or a different VM. Test that you can read the dump back on another machine before you trust it.

The commands below are the minimum LAMP safety net used for this walkthrough. Adapt the database tool, the config paths and the app directories to your own stack before running:

# Example for the LAMP workload in this guide.
# Swap mariadb-dump/paths for your stack's equivalents.
sudo mariadb-dump "${DB_NAME}" > "${BACKUP_DIR}/${DB_NAME}-pre-upgrade.sql"
sudo tar czf "${BACKUP_DIR}/etc-pre-upgrade.tar.gz" /etc/apache2 /etc/php /etc/mysql 2>/dev/null
sudo tar czf "${BACKUP_DIR}/www-pre-upgrade.tar.gz" /var/www/html
sudo dpkg --get-selections > "${BACKUP_DIR}/dpkg-selections.txt"
ls -lh "${BACKUP_DIR}"

Take a hypervisor snapshot too if you are on Proxmox, VMware, or a cloud provider that supports them. The hypervisor-level snapshot is the only rollback that actually works for a broken distro upgrade. There is no supported apt downgrade path. If your workload has a maintenance window, stop the application services before you snapshot so the on-disk state is quiesced rather than mid-transaction.

Step 4: Fully update 22.04 before upgrading

Two things the upgrade tool insists on. The kernel modules it is about to replace must match a kernel that is actually booted, and the package state has to be clean. If you have a pending reboot from a prior apt upgrade, do-release-upgrade refuses with a clear error:

sudo apt update
sudo apt -y upgrade
sudo apt -y autoremove
[ -f /var/run/reboot-required ] && echo "REBOOT REQUIRED" || echo "no reboot needed"
sudo reboot

Reconnect after the reboot. If you skip the reboot and try to upgrade anyway, the tool exits with the message You have not rebooted after updating a package which requires a reboot. Please reboot before upgrading. This caught the first run on the test VM, adding five minutes to the timing.

Step 5: First hop, 22.04 to 24.04

Set the upgrade channel to lts, confirm the release offered, then run the upgrade. Use the non-interactive frontend so it does not stall on Apache, Nginx or SSH config prompts, it keeps local versions by default:

sudo sed -i 's/^Prompt=.*/Prompt=lts/' /etc/update-manager/release-upgrades
sudo do-release-upgrade -c

You should see New release '24.04.4 LTS' available. Run 'do-release-upgrade' to upgrade to it. Kick off the actual upgrade in the non-interactive frontend. This takes roughly 20 to 30 minutes depending on CPU, network and disk speed:

sudo DEBIAN_FRONTEND=noninteractive do-release-upgrade \
  -f DistUpgradeViewNonInteractive

Three things to know while it runs. SSH will briefly reject connections when openssh-server is reconfigured, give it 90 seconds and it comes back. Apache gets restarted multiple times while new modules are unpacked. The tool logs every config file it replaces under /var/log/dist-upgrade/, worth a grep afterwards for Installing new version of config file to see what changed.

When it finishes the tool does not reboot for you under DistUpgradeViewNonInteractive, reboot manually:

sudo reboot

Reconnect after the reboot and confirm the kernel has flipped to the 6.8 series before moving on.

Step 6: Fix the Apache PHP module gotcha

This is the one predictable failure in a 22.04 → 24.04 hop on any LAMP box. 22.04 ships PHP 8.1, 24.04 ships PHP 8.3. The upgrade installs libapache2-mod-php8.3 but leaves the old php8.1.load and php8.1.conf symlinks in /etc/apache2/mods-enabled/, so Apache on the first boot after the upgrade fails with:

apache2: Syntax error on line 146 of /etc/apache2/apache2.conf:
Syntax error on line 3 of /etc/apache2/mods-enabled/php8.1.load:
Cannot load /usr/lib/apache2/modules/libphp8.1.so into server:
/usr/lib/apache2/modules/libphp8.1.so: cannot open shared object file:
No such file or directory

The fix is two a2*mod calls and a restart. No manual editing:

sudo a2dismod php8.1
sudo a2enmod php8.3
sudo systemctl restart apache2
systemctl is-active apache2

The full error and fix captured on the test VM:

Apache PHP module error after the 22.04 to 24.04 upgrade and the a2dismod/a2enmod fix

On the test VM the output from systemctl is-active apache2 went from failed to active in under two seconds. The workload curl http://localhost/api/stats.php returned php_version: 8.3.6, server: Linux 6.8.0-110-generic and the same 500 / 120 / 2000 row counts as before the upgrade.

Step 7: Validate the workload before the second hop

Never run the 24.04 → 26.04 hop until you have confirmed the workload on 24.04 is working. A broken app on 24.04 will not magically heal on 26.04, it will be twice as hard to diagnose with two sets of changes mixed together. Run the app’s health checks, read the logs, exercise the write path:

systemctl is-active apache2 mariadb
curl -s http://localhost/api/stats.php | python3 -m json.tool
sudo mariadb -e "SELECT (SELECT COUNT(*) FROM ${DB_NAME}.customers) c, \
  (SELECT COUNT(*) FROM ${DB_NAME}.products) p, \
  (SELECT COUNT(*) FROM ${DB_NAME}.orders) o, \
  (SELECT ROUND(SUM(total),2) FROM ${DB_NAME}.orders WHERE status<>'cancelled') rev"

On the test VM the fastfetch snapshot after the first reboot looks like this, PHP is 8.3, kernel is 6.8, OpenSSH is 9.6p1:

fastfetch output on Ubuntu 24.04 LTS after the first upgrade hop showing kernel 6.8, bash 5.2 and OpenSSH 9.6p1

With 24.04 stable and the workload verified, start the second hop.

Step 8: Second hop, 24.04 to 26.04

Until 26.04.1 ships on August 6 2026, do-release-upgrade on a plain LTS channel will say “No new release found.” The -d flag tells it to include development releases:

sudo apt update
sudo apt -y upgrade
sudo apt -y autoremove
sudo do-release-upgrade -c --devel-release

If that prints New release '26.04 LTS' available. you are good to proceed. Run the full upgrade with the same non-interactive frontend as before, and the -d flag to pick up 26.04:

sudo DEBIAN_FRONTEND=noninteractive do-release-upgrade -d \
  -f DistUpgradeViewNonInteractive

This is the longer of the two hops because 26.04 brings a much larger change: kernel 7.0, systemd 259, PHP 8.5, Apache 2.4.66, bash 5.3, OpenSSH 10.2 with post-quantum KEX enabled by default, Rust coreutils replacing GNU coreutils on fresh installs, and cgroup v1 dropped entirely from the stack. Total download is around 1.4 GB for a LAMP host like this one.

Reboot once it completes:

sudo reboot

After the VM comes back up, log in and check the workload one more time.

Step 9: Validate on 26.04

Unlike the first hop, the 24.04 → 26.04 transition handled the Apache PHP module swap cleanly on the test VM, because 24.04 → 26.04 is a single PHP minor step (8.3 → 8.5) and the postinst scripts in libapache2-mod-php8.5 disable the older 8.3 module automatically. Worth checking anyway:

ls /etc/apache2/mods-enabled/ | grep php
systemctl is-active apache2 mariadb
curl -s http://localhost/api/stats.php | python3 -m json.tool

If mods-enabled still shows a php8.3.load symlink, run sudo a2dismod php8.3 && sudo a2enmod php8.5 && sudo systemctl restart apache2 and re-check.

Here is the fastfetch snapshot at the end of the second hop. Everything on the test VM now reports 26.04 signatures, the workload kept its data, and the PHP dashboard renders against a kernel 7.0 host:

fastfetch output on Ubuntu 26.04 LTS after the second upgrade hop showing kernel 7.0, bash 5.3 and OpenSSH 10.2p1

And the JSON API response captured on all three OS versions back to back, same counts, same revenue, different PHP and kernel strings:

JSON API response from the PHP dashboard on Ubuntu 22.04, 24.04 and 26.04 showing identical customer/product/order counts and identical revenue

The PHP-rendered dashboard is identical against the live 26.04 host, top-5 products by revenue, 500 customer rows, 120 products, 2000 orders, served by Apache 2.4.66 with libphp8.5.so:

PHP inventory dashboard rendered on Ubuntu 26.04 LTS with PHP 8.5.4 on kernel 7.0 showing the same 500/120/2000 counts and revenue totals

With the workload confirmed, sweep up the leftover state from the two hops.

Step 10: Post-upgrade cleanup

Four cleanup actions that keep the box tidy. Purge the old kernel images that do-release-upgrade leaves around, remove obsolete package configs, rebuild the initramfs so Dracut is consistent, and update GRUB:

sudo apt -y autoremove --purge
sudo apt -y autoclean
dpkg -l | awk '/^rc/ {print $2}' | xargs -r sudo dpkg --purge
sudo update-initramfs -u -k all
sudo update-grub
df -h /boot /

On the test VM, / went from 2.4 GiB used on 22.04 to 4.4 GiB used on 26.04 after cleanup. The jump is almost entirely from bigger kernel modules plus the x86-64-v3 optimised package set that 26.04 pulls in on AVX2-capable CPUs.

Troubleshooting the real errors that came up

Three errors hit the test VM during this upgrade. All three are in the /var/log/dist-upgrade/main.log tail so you can grep for the same strings on your own runs.

Error: “You have not rebooted after updating a package which requires a reboot”

This blocked the first do-release-upgrade invocation. Cause: an earlier apt upgrade installed a new 5.15 kernel and the VM was still running the old one. Fix: sudo reboot, reconnect, retry. If you cannot reboot the host, run sudo unattended-upgrade --dry-run -d to see what still needs a restart and clear it first.

Error: “Cannot load /usr/lib/apache2/modules/libphp8.1.so”

Apache refused to start after the first reboot onto kernel 6.8. The upgrade installed libapache2-mod-php8.3 but left mods-enabled/php8.1.load pointing at a missing shared object. Fix: sudo a2dismod php8.1 && sudo a2enmod php8.3 && sudo systemctl restart apache2. If you run a different PHP extension stack (e.g. PHP-FPM via FastCGI), also re-check /etc/apache2/conf-enabled/php*-fpm.conf.

Warning: “Download is performed unsandboxed as root”

Harmless, prints once during the metadata fetch. It is a systemd hardening warning from a tightened apt sandbox profile in 24.04+ when the upgrade tool runs as root. Ignore it, the download still verifies the GPG signature.

Series navigation

Related Ubuntu 26.04 guides on computingforgeeks:

Related Articles

Git Install Gogs Git service on Ubuntu 22.04|20.04|18.04 Prometheus Monitor Apache with Prometheus and Grafana in 5 minutes Ubuntu Install Caddy Web Server on Ubuntu 26.04 LTS Monitoring How To Install Zabbix 7.0 on Ubuntu 24.04|22.04

Leave a Comment

Press ESC to close