Upgrade Ubuntu 22.04 to 26.04 (via 24.04)
There is no direct jump. Upgrade 22.04 to 24.04, verify and snapshot, then take the 26.04 hop. Covers the PPA, PHP, Python, database and netplan breaks.
There is no direct upgrade from Ubuntu 22.04 to 26.04
There is no direct upgrade from Ubuntu 22.04 LTS to 26.04 LTS. do-release-upgrade moves one LTS at a time, so the path is 22.04 to 24.04, then 24.04 to 26.04, with a reboot and a full check between the two. The 26.04 release notes say it plainly: "If you're currently using an older Ubuntu LTS or interim release, such as Ubuntu 22.04 LTS or 25.04, you must first upgrade to either Ubuntu 24.04 LTS or 25.10 before you can proceed to 26.04 LTS."
The second hop has a gate. Canonical offers an LTS-to-LTS upgrade only once the target's first point release exists, and 26.04.1 shipped on 27 August 2026. As of 18 September 2026 the file do-release-upgrade reads, https://changelogs.ubuntu.com/meta-release-lts, lists 26.04.1 LTS with Supported: 0, so a 24.04 box still prints No new release found. The 26.04.1 announcement said the upgrade would open "a couple of weeks" later, after backports for a regression in rust-coreutils. Check the gate yourself with do-release-upgrade -c rather than trusting a date. Why LTS upgrades wait for the .1 point release covers the reasoning behind that gate.
Two hops means two reboots and two rounds of disabled third-party repositories, with four years of major-version jumps in PHP, Python, PostgreSQL and the kernel landing on one server. That is where this guide spends most of its time. Hop two itself is documented step by step in the 24.04 to 26.04 upgrade guide, so it is not repeated here. The reason to start now: 22.04 standard support ends in April 2027, and after that only Ubuntu Pro's expanded security maintenance delivers patches to it.
Why the upgrader refuses to skip a release
do-release-upgrade does not carry the upgrade logic itself. It downloads a tarball named in the UpgradeTool line of the meta-release file, noble.tar.gz for 24.04 and resolute.tar.gz for 26.04, and that tarball holds a script written for exactly one starting release. Every package's maintainer scripts, the preinst and postinst hooks that migrate config files, are tested for the upgrade from the previous release and nothing else. Jumping 22.04 to 26.04 would run the 26.04 scripts against configuration states they have never seen, and dpkg would stop half way with a broken system.
Do not try to route around the gate with Prompt=normal in /etc/update-manager/release-upgrades. From 22.04 that setting points at 22.10, and 22.10, 23.04 and 23.10 are all end of life with their archives moved to old-releases.ubuntu.com, so apt update fails at the first step. Keep Prompt=lts. If a 24.04 box tells you No new release found after the gate has opened, the reasons do-release-upgrade finds nothing are almost always that file or a held metapackage.
Upgrade in place, or build a fresh 26.04 VPS?
An in-place upgrade keeps your data and your configuration, and costs two maintenance windows. A fresh 26.04 VPS plus a migration costs more work up front and leaves you with a clean box. Pick the migration when any of these is true.
- The server has already been upgraded in place once, from 20.04 or earlier. Each hop leaves obsolete packages and stale config behind, and three hops of leftovers are hard to debug.
- More than two or three third-party repositories or PPAs are enabled. Each one is disabled on each hop, and each must be checked for a build against the new codename.
- A control panel or an out-of-tree kernel module is installed. These break on the kernel jump and are outside what
do-release-upgradecan fix. - You cannot take a snapshot. Without a rollback point, an in-place upgrade of a production box is a risk you cannot undo.
Moving a server to a new VPS walks through the copy and the cutover. The rest of this guide assumes you chose the in-place path.
Before hop one: snapshot, space and a clean 22.04
Bring 22.04 fully up to date first. The upgrader refuses to start otherwise, with Please install all available updates for your release before upgrading.
lsb_release -a
sudo apt update && sudo apt full-upgrade -y
sudo apt autoremove --purge -y
sudo rebootAfter the reboot, lsb_release -rs should print 22.04 and uname -r should show the kernel you expect: 5.15.x on the GA series or 6.8.x on the HWE series. If you are unsure which series you run, the GA versus HWE kernel explainer shows how to tell and why it matters for an upgrade.
Check disk space. The upgrader downloads every package before it installs any, and a separate /boot partition holding several old kernels is the most common place to run out.
df -h / /boot
dpkg -l 'linux-image-*' | grep ^iiAim for at least 5 GB free on /. If /boot has under 200 MB free, remove the old kernels before you start, not after the upgrader has aborted.
Now take a snapshot in your provider's panel and wait for it to finish. A snapshot is a rollback point for the whole disk, which is exactly what a failed release upgrade needs. It is not a backup, and the difference between a snapshot and a backup matters here: keep your normal backups running too. Then record the state you are leaving.
apt-mark showmanual > ~/manual-packages-22.04.txt
apt-cache policy | grep -oE 'https?://[^ ]+' | sort -u > ~/apt-sources-22.04.txt
ls /etc/apt/sources.list.d/
php -v; python3 --version; node -v; psql --version; mysql --version
sudo iptables-save > ~/iptables-22.04.rules
sudo cp -a /etc/netplan ~/netplan-22.04Some of those commands print command not found when that stack is not installed. That is fine. The point is a written record of what ran on 22.04, because after two hops you will not remember which PHP version an old site expected.
Finally, confirm /etc/update-manager/release-upgrades reads Prompt=lts, and open a second SSH session to the box. Keep it open through the whole upgrade.
Hop one: 22.04 to 24.04
sudo do-release-upgrade -cExpected output:
Checking for a new Ubuntu release
New release '24.04.5 LTS' available.
Run 'do-release-upgrade' to upgrade to it.Start with sudo do-release-upgrade. Over SSH, the first prompt is this one:
This session appears to be running under ssh. It is not recommended
to perform a upgrade over ssh currently because in case of failure it
is harder to recover.
If you continue, an additional ssh daemon will be started at port
'1022'.
Do you want to continue?Say yes, then open that port from your second session, because the upgrader tells you it will not do it for you:
sudo ufw allow 1022/tcpIf you run nftables or raw iptables instead of ufw, add the equivalent rule. Also open port 1022 in your provider's network firewall if it has one, since that is a separate control from the box.
The upgrader runs inside a screen session, so a dropped connection does not kill it. Reconnect and reattach with sudo screen -ls followed by sudo screen -r. It then reads the package lists and shows a summary: how many packages it will install and remove, and how long the download will take. Read the "remove" number. Anything above a few dozen on a plain server is worth pressing d for details before you continue.
Three prompts matter during the run.
Third party sources disabled. The upgrader comments out every apt source it does not recognise as Ubuntu's own and marks each line # disabled on upgrade to noble. It has to, because a line that says jammy would offer packages built for the old release. You will re-enable them one at a time at the checkpoint.
Configuration file prompts. For any config file you edited, /etc/ssh/sshd_config is the usual one, dpkg asks whether to install the package maintainer's version. The default, N, keeps yours. Take the default for sshd_config unless you know why not, because the maintainer's version resets PasswordAuthentication and Port, and you are connected through that daemon.
Remove obsolete packages? This removes packages that exist in 22.04 but not in 24.04. Answer N for now if you run PostgreSQL, because postgresql-14 is on that list and pg_upgradecluster needs its binaries to migrate the data. You can remove obsolete packages after the checkpoint with sudo apt autoremove --purge.
Expect 30 to 60 minutes on a small VPS, most of it dpkg unpacking rather than downloading. When it finishes, it asks to reboot. Say yes.
The checkpoint between the hops
Do not start hop two until this list is clean. Two rounds of breakage stacked on top of each other are far harder to debug than one, and 24.04 is supported until April 2029, so pausing here for a week costs nothing.
lsb_release -rs
uname -r
systemctl --failed
sudo apt update && sudo apt full-upgrade -ylsb_release -rs must print 24.04 and uname -r should start with 6.8. systemctl --failed should print 0 loaded units listed. Any unit it does list is your first job.
Then check apt sources. The noble upgrader converts /etc/apt/sources.list into /etc/apt/sources.list.d/ubuntu.sources, the deb822 format, and leaves the old file holding a comment that points at the new one. If you later paste a 22.04-style deb http://... line back into sources.list, apt sees both files and prints is configured multiple times. That duplicate-source error has one fix: delete the old-format line.
grep -r 'disabled on upgrade' /etc/apt/sources.list /etc/apt/sources.list.d/For each disabled line, decide. If the vendor publishes a noble suite, change jammy to noble, remove the comment marker, and run sudo apt update. If it prints E: The repository '... noble Release' does not have a Release file., the vendor has no build for 24.04 and you keep the line disabled or find another source. Do the same for PPAs. Then work through every item in the next section that applies to your server, reboot once more, confirm every service you care about answers, and take a fresh snapshot. Name it so you can tell it from the 22.04 snapshot, and keep both until 26.04 is verified.
What breaks across two hops
Each of these is a normal 24.04 problem or a normal 26.04 problem on its own. Doing both hops back to back means they arrive together, so fix each one at the checkpoint, before hop two doubles it.
Third-party repositories and PPAs are disabled twice
Every non-Ubuntu source is commented out on hop one and again on hop two, where the marker reads # disabled on upgrade to resolute. A PPA built only for jammy and noble will not have a resolute suite on day one, and the does not have a Release file error is how you find out. On a web server the ones you will meet most often are Docker CE from download.docker.com, NodeSource, ppa:ondrej/php, and the Tailscale repository, and all of them except NodeSource name the release codename in their apt line. The current NodeSource setup writes nodistro as the suite, so it survives both hops untouched. Docker's repository needs the codename edit twice. Alternatively, the docker.io package in the 26.04 archive is version 29, which is current enough that many boxes no longer need the vendor repository at all. If Tailscale's line is one of yours, the Tailscale install errors on Ubuntu are exactly the ones a stale codename produces.
PHP jumps two majors: 8.1, then 8.3, then 8.5
Ubuntu 22.04 ships PHP 8.1, 24.04 ships 8.3, and 26.04 ships 8.5. If you installed php-fpm through the unversioned metapackage, hop one installs php8.3-fpm beside php8.1-fpm and starts both. Nothing switches your web server over, so nginx keeps talking to the 8.1 socket and keeps working, until the day you remove obsolete packages and the error log fills with:
connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory) while connecting to upstreamThe fix is to point fastcgi_pass at unix:/run/php/php8.3-fpm.sock, run sudo nginx -t, and reload. On Apache with mod_php, run sudo a2dismod php8.1 && sudo a2enmod php8.3 && sudo systemctl restart apache2. Do it at the checkpoint on 8.3, run the application's tests, then repeat for 8.5 after hop two. An application that cannot run on 8.5 yet is the one real reason to re-enable ppa:ondrej/php on 26.04, because that PPA carries every PHP version for every current Ubuntu release. The same steps apply to a LAMP stack built on 24.04, which only sees the second half of this jump.
Python: 3.10 to 3.12 to 3.14, and pip stops writing to the system
The system Python moves from 3.10 to 3.12 on hop one and to 3.14 on hop two. Two things break. First, every virtualenv created on 22.04 has bin/python as a symlink to /usr/bin/python3.10, which is removed with the obsolete packages, so the venv fails with No such file or directory. Rebuild each one with python3 -m venv --clear .venv and reinstall its requirements. Second, from 24.04 on, pip install outside a venv refuses with:
error: externally-managed-environmentThat is PEP 668, and it protects the Python packages apt manages from being overwritten. Use a venv, or apt install python3-<name>, rather than the --break-system-packages flag the error message mentions.
Node: 12 to 18 from the archive, or NodeSource
The archive's nodejs package is 12.22 on 22.04 and 18.19 on 24.04, so an application built against Node 12 meets four years of removed APIs at once. Most production boxes use NodeSource instead, and NodeSource pins a specific major, so the version does not change on either hop. Only the repository line needs re-enabling.
Databases: PostgreSQL 14 to 16 to 18, and MySQL 8.4 locks accounts
Hop one installs PostgreSQL 16 next to your 14 cluster and gives the new, empty cluster port 5433. pg_lsclusters shows both. Your data is still in 14 on port 5432. Migrate it at the checkpoint:
pg_lsclusters
sudo pg_dropcluster --stop 16 main
sudo pg_upgradecluster 14 main
pg_lsclusterspg_upgradecluster dumps 14 and loads it into a new 16 cluster on port 5432, then leaves 14 stopped on 5433. Check the application, then sudo pg_dropcluster 14 main. After hop two the archive ships PostgreSQL 18, and you repeat the same four commands with 16 and 18. This is why you answered N to removing obsolete packages: pg_upgradecluster runs the old server's binaries to read the old data.
MySQL is 8.0 on both 22.04 and 24.04, so hop one changes nothing. Hop two moves to 8.4, which disables the mysql_native_password plugin, and the release notes say accounts that use it "will be locked out by default". The client sees ERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded. Find those accounts at the checkpoint, while you are still on 8.0:
SELECT user, host, plugin FROM mysql.user WHERE plugin = 'mysql_native_password';
ALTER USER 'app'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'the-same-password';Old PHP and Python drivers that cannot speak caching_sha2_password are the reason those accounts exist, so test the application after each ALTER USER.
22.04-era netplan files
Many 22.04-era VPS images shipped /etc/netplan/50-cloud-init.yaml with gateway4: and world-readable permissions. Netplan 1.0 on 24.04 and 1.2 on 26.04 still accept both, with warnings you will see the first time you run sudo netplan generate:
** (generate:1234): WARNING **: 10:21:07.512: Permissions for /etc/netplan/50-cloud-init.yaml are too open. Netplan configuration should NOT be accessible by others.
** (generate:1234): WARNING **: 10:21:07.513: `gateway4` has been deprecated, use default routes instead.Fix the permissions with sudo chmod 600 /etc/netplan/*.yaml. Replace gateway4: 203.0.113.1 with a default route:
routes:
- to: default
via: 203.0.113.1Test with sudo netplan try, not netplan apply. try applies the change and reverts it after 120 seconds unless you press Enter, so a mistake that kills your SSH session reverts on its own. Do this at the checkpoint on 24.04, because a netplan mistake found after hop two looks exactly like a hop-two problem.
iptables-legacy survives both hops
Ubuntu has used the nftables backend for iptables since 20.10. A 22.04 box that was switched to legacy for an old Docker or a firewall script, with update-alternatives --set iptables /usr/sbin/iptables-legacy, keeps that choice through both upgrades, because update-alternatives never overrides a manual selection. Check it:
sudo iptables -V
sudo update-alternatives --display iptables | head -3
sudo nft list ruleset | headiptables v1.8.x (legacy) means the legacy backend. It keeps working, and that is the trap: rules loaded through the legacy tools and rules loaded through nftables both run in the kernel, and neither tool shows the other's, so nft list ruleset comes back nearly empty while the legacy rules keep dropping packets. Docker 29 on 26.04 can be switched to an nftables backend, and ufw follows whichever alternative is selected, so a mixed box ends up with two rulesets nobody can read in one place. Move to iptables-nft at the checkpoint with sudo update-alternatives --set iptables /usr/sbin/iptables-nft, reload your saved rules, and confirm sudo nft list ruleset now shows them.
Kernel 5.15 to 6.8 to 7.0, and a new initramfs tool
The GA kernel goes from 5.15 to 6.8 on hop one and to 7.0 on hop two. Old kernels are not removed by the upgrade, so /boot fills up, and a VPS that fails to boot after a kernel change is the failure mode a snapshot exists for. Recovering a VPS that will not boot after a kernel update covers the console and the GRUB menu. Fresh 26.04 installs build their initramfs with dracut instead of initramfs-tools. An upgraded box may keep initramfs-tools; check with dpkg -l initramfs-tools dracut and leave whichever is installed alone.
Two more 26.04 changes land on hop two and deserve reading before you start it. sudo is now sudo-rs, and what changes with sudo-rs matters if you have a complex sudoers. The coreutils are the Rust rewrite, and the Rust coreutils explainer covers the flags that differ. Also, systemd 259 no longer supports cgroup v1. If /proc/cmdline contains systemd.unified_cgroup_hierarchy=0 from an old container setup, remove it from GRUB before hop two, because on 26.04 it can no longer do what it was put there to do.
Hop two: 24.04 to 26.04
Only start when the gate is open:
sudo do-release-upgrade -cNew release '26.04.1 LTS' available. means Canonical has flipped the meta-release entry, and you run sudo do-release-upgrade exactly as in hop one, with the second sshd on port 1022 and the same prompts. The third-party marker this time reads # disabled on upgrade to resolute. No new release found means the gate is still closed. do-release-upgrade -d forces the upgrade early by reading the development meta-release file. The 26.04.1 announcement mentioned it for people who cannot wait, but on a server it means opting in before the regression fixes Canonical said it was waiting on, so wait. The full procedure, with the 26.04-specific prompts and the post-upgrade checks, is in the 24.04 to 26.04 guide. Follow it from the snapshot step, since you already have the checkpoint snapshot.
If a hop fails
The upgrader logs to /var/log/dist-upgrade/. main.log holds the upgrader's own decisions and term.log holds every dpkg line; apt.log is the dependency solver's view. A hop that stops half way usually leaves apt in a state that sudo dpkg --configure -a followed by sudo apt -f install can finish. If it cannot, recovering a failed release upgrade goes through the rescue path, and the snapshot you took before that hop is the last resort. Restore the 24.04 snapshot after a failed hop two and you lose nothing but the time. Restore the 22.04 snapshot after a failed hop one and you are back where you started, with a list of what to fix before trying again.
FAQ
Can I upgrade Ubuntu 22.04 directly to 26.04?
No. do-release-upgrade moves one LTS at a time and downloads an upgrader written for exactly one starting release. From 22.04 the only supported path is 22.04 to 24.04, then 24.04 to 26.04. Setting Prompt=normal to chain through interim releases fails too, because 22.10, 23.04 and 23.10 are end of life and their archives have moved to old-releases.ubuntu.com.
Why does do-release-upgrade on 24.04 say "No new release found"?
Canonical opens an LTS-to-LTS upgrade only after the target's first point release, and only when the meta-release-lts file marks it Supported: 1. Ubuntu 26.04.1 shipped on 27 August 2026, but as of 18 September 2026 the entry still reads Supported: 0 while a rust-coreutils regression fix is backported. Run sudo do-release-upgrade -c again in a week. Also confirm /etc/update-manager/release-upgrades reads Prompt=lts, not never.
Should I upgrade in place or build a fresh 26.04 VPS?
Upgrade in place when the box is a single-purpose server with few third-party repositories and you can take a snapshot before each hop. Build a fresh 26.04 VPS and migrate when the server has been upgraded in place before, runs a control panel, uses a custom kernel, or depends on several PPAs, because each of those multiplies the work at both hops.
How long does the 22.04 to 26.04 upgrade take?
Plan for two maintenance windows of 30 to 60 minutes each on a small VPS, plus the time between them to work through the checkpoint. The checkpoint is where the real time goes, because that is where you migrate PostgreSQL, switch PHP sockets, rebuild Python virtualenvs and re-enable repositories. There is no penalty for leaving weeks between the hops: 24.04 is supported until April 2029.
What happens to my PPAs during the upgrade?
The upgrader comments out every non-Ubuntu source and marks each line # disabled on upgrade to noble, then does it again with resolute on hop two. A PPA line names a release codename, so re-enable it by changing the codename and running sudo apt update. If apt prints does not have a Release file, the PPA has no build for that release yet, and you either wait or find another source.