SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor

Upgrade Ubuntu 24.04 to 26.04 on a VPS

Ubuntu 24.04 does not offer 26.04 until the 26.04.1 point release lands. Here is the safe upgrade order, and the server services that break.

When can you upgrade Ubuntu 24.04 to 26.04?

You can upgrade Ubuntu 24.04 to 26.04 on a VPS once the 26.04.1 point release ships, scheduled for 27 August 2026. Until then a 24.04 server will not see the new release, on purpose. Ubuntu 26.04 LTS (Resolute Raccoon) was released on 23 April 2026, but Canonical only opens the LTS to LTS upgrade path at the first point release, because that release rolls up the install and upgrade bugs found in the first months.

Run the check on a 24.04 box in early August 2026 and you get this:

sudo do-release-upgrade
Checking for a new Ubuntu release
No new release found.

That is not a fault on your server. /etc/update-manager/release-upgrades contains Prompt=lts on Ubuntu Server, which means the tool offers only the next long term support release, and only once its .1 point release exists. Setting Prompt=normal would instead walk you through 24.10, 25.04 and 25.10 in turn, interim releases that have all reached end of life. Leave it on lts and wait. Dates on Canonical's schedule can move, so check it again if the day passes quietly.

Every command below is one you run yourself, on your own server, in the order given. A release upgrade cannot be rehearsed on the machine you are upgrading. It replaces the kernel and the C library, and it needs a reboot to finish.

Should you upgrade at all?

Ubuntu 24.04 receives standard security updates into 2029, so a working production server is under no deadline. Upgrade because you want something 26.04 carries: PHP 8.5, PostgreSQL 18, MySQL 8.4 LTS, OpenSSH 10.2 or the 7.0 kernel. "The number went up" is not a reason to touch a machine that serves customers.

Do not upgrade in place when any of these is true:

  • You have never opened your provider's console (VNC or serial) and logged in through it. That console is the only way back into the box if SSH breaks, and finding out it does not work while you are locked out is too late.
  • You cannot afford an hour of downtime and have no rollback.
  • Your stack depends on a third-party repository that has not published for resolute yet.
  • The server was built by hand over two years and nobody knows what is on it.

The alternative is often better: build a fresh 26.04 VPS, install your stack and restore the data, then switch DNS once it answers correctly. You keep the old server running until the new one has proved itself, and rollback is a DNS change instead of a restore. If you go that way, start with the first ten minutes on a new VPS and build the new box properly.

Step 1: take a backup you can restore from

Use two layers, because they fail in different ways. A provider snapshot covers the whole disk and restores in minutes, but it is taken while your databases are writing, so it is crash consistent rather than application consistent. A file-level backup with restic, stored off the server gives you single files and a copy that survives your account being locked.

Dump the databases by hand first. A dump is the only backup of a database you can trust without stopping it.

sudo -u postgres pg_dumpall | sudo tee /var/backups/pg-all.sql > /dev/null
sudo mysqldump --all-databases --single-transaction --routines | sudo tee /var/backups/mysql-all.sql > /dev/null
sudo tar czf /var/backups/etc-before-upgrade.tgz -C / etc
sudo chmod 600 /var/backups/pg-all.sql /var/backups/mysql-all.sql

--single-transaction gives a consistent dump only for InnoDB tables. MyISAM tables need the database stopped. The /etc tarball is the one you will actually reach for, because it holds every config file the upgrade is about to ask you questions about.

A backup you have never restored is a guess. Pull one file out of it now, before you need it under pressure.

Step 2: patch 24.04 completely first

do-release-upgrade refuses to run on a system with a broken package state, and a half-patched 24.04 makes every later failure harder to read.

sudo apt update
sudo apt full-upgrade
sudo apt --purge autoremove
sudo dpkg --audit
apt-mark showhold

dpkg --audit printing nothing means no package is half configured. apt-mark showhold printing nothing means no package is pinned to a version that would block the upgrade. Release anything it lists with sudo apt-mark unhold and the package name, or accept that the hold exists for a reason and stop here.

Reboot if the kernel changed, so you upgrade from a machine running the code it thinks it is running.

[ -f /var/run/reboot-required ] && sudo reboot

Then check disk space. The upgrader downloads the entire new package set before it installs anything, and it aborts with a message naming the filesystem if there is not enough room.

df -h / /boot

Under about 5 GB free on / is where this goes wrong. A /boot under 300 MB fails later, during the kernel install, with No space left on device. Old kernels are usually the cause, and sudo apt --purge autoremove clears them.

One more thing to stop before you start: if automatic security updates fire mid-run they hold the dpkg lock, and the release upgrader stops with Could not get lock /var/lib/dpkg/lock-frontend. Run sudo systemctl stop unattended-upgrades first and start it again when you are done.

Step 3: check third-party repositories and pinned packages

do-release-upgrade disables every apt source that is not an Ubuntu one, because a package built for noble can break a resolute system. It re-enables the ones it recognises afterwards and leaves the rest commented out. Know what you are carrying before the tool decides for you.

ls /etc/apt/sources.list.d/
grep -rhE '^(deb |Types:|URIs:|Suites:)' /etc/apt/sources.list.d/
ubuntu-security-status --thirdparty
ls /etc/apt/preferences.d/

Ubuntu 24.04 uses two formats in that directory: the old one-line .list files, and deb822 .sources files with Types: and Suites: fields. Both are disabled by the upgrade. ubuntu-security-status --thirdparty lists the installed packages that no Ubuntu archive provides, which is the honest count of what you have bolted on. Anything in /etc/apt/preferences.d/ is a pin, and a pin written for noble will keep choosing an old package on the new release.

For each third-party repository, confirm the vendor has published for the new codename before you begin. Docker's suites are listed at https://download.docker.com/linux/ubuntu/dists/, and other vendors expose the same directory. A source pointed at a suite that does not exist gives this on the first apt update after the upgrade:

E: The repository 'https://download.docker.com/linux/ubuntu resolute Release' does not have a Release file.

Leave that source disabled until the vendor publishes. Editing the codename to one the vendor did build is how you install packages linked against the wrong system libraries.

Step 4: run the upgrade in tmux, not in a plain SSH shell

If your connection drops while do-release-upgrade runs in a plain login shell, the process receives SIGHUP and dies partway through unpacking. That leaves dpkg half configured, and a box that may no longer have a working network stack to reconnect to. Run it inside a terminal multiplexer instead, so the process stays alive on the server when your client goes away.

sudo apt install -y tmux
tmux new -s upgrade

Inside that session:

sudo ufw allow 1022/tcp
sudo do-release-upgrade

The upgrader starts a second SSH daemon on port 1022 before it changes anything, and says so:

To make recovery in case of failure easier, an additional sshd will be started on port '1022'. If anything goes wrong with the running ssh you can still connect to the additional one.

It does not open your firewall for that port, because punching a hole in a firewall without asking would be a bad surprise. Open 1022 yourself before you start, and close it when you are finished with sudo ufw delete allow 1022/tcp. Remember your provider may run a second firewall in its control panel, outside the server.

If the connection drops anyway, log back in and run tmux attach -t upgrade. The upgrade kept running while you were gone.

Step 5: answer the configuration file prompts deliberately

dpkg only prompts for files you or a script changed. Every prompt is therefore a file you edited on purpose, and pressing enter to make it go away is how a hardened server quietly becomes a default one.

Configuration file '/etc/ssh/sshd_config'
 ==> Modified (by you or by a script) since installation.
 ==> Package distributor has shipped an updated version.
   What would you like to do about it ?  Your options are:
    Y or I  : install the package maintainer's version
    N or O  : keep your currently-installed version
      D     : show the differences between the versions
      Z     : start a shell to examine the situation
 The default action is to keep your current version.
*** sshd_config (Y/I/N/O/D/Z) [default=N] ?

Press D first, every time. Read what changed, then keep your version with N. The default is already N, which is the safe answer, because your file works today and the packaged one has never run on this machine.

Keeping your file has a cost: you do not get the new defaults. Reconcile afterwards, once the box is up and you are not under time pressure.

sudo find /etc -name '*.dpkg-dist' -o -name '*.dpkg-new'

Each file that lists is the maintainer's version, saved next to yours. Diff them one at a time and copy across the settings that matter. Two are worth extra care: /etc/ssh/sshd_config, because the wrong answer ends your session, and your web server config, because the wrong answer takes the sites down.

The upgrade also asks which services to restart, through needrestart. Accept the full list. A daemon still running against a shared library file that has been deleted from disk will crash on some later request, at a time you are not watching.

Step 6: reboot, then check the machine

sudo reboot

When it comes back:

lsb_release -a
uname -r
systemctl --failed
journalctl -p err -b --no-pager | head -50
sudo apt update && sudo apt full-upgrade
sudo apt --purge autoremove

lsb_release -a should report Release: 26.04 and Codename: resolute. uname -r should show a 7.0 kernel. systemctl --failed should list zero units, and anything it does list is your next job. The final apt update pulls the updates published since the release images were built.

PostgreSQL 16 to 18: the cluster that quietly stays behind

Ubuntu 24.04 ships PostgreSQL 16 and 26.04 ships PostgreSQL 18. The upgrade installs 18 next to 16 and does not move your data. Debian's postgresql-common layer creates a new empty cluster for the new major version on the next free port, so 16 keeps port 5432 with all your data and 18 sits empty on 5433. Your application carries on talking to 5432 and nothing looks wrong, which is why people find this months later.

pg_lsclusters

Two clusters listed means you have not migrated. Do it when you can stop the application:

sudo pg_dropcluster --stop 18 main
sudo pg_upgradecluster 16 main
pg_lsclusters
sudo -u postgres vacuumdb --all --analyze-only

Drop the empty 18 cluster first, because pg_upgradecluster will not write into a target cluster that already exists. The default method dumps 16 and reloads it into 18, so you need free disk space roughly the size of the database. -m upgrade uses pg_upgrade instead and is far faster on a large database. When it finishes, read the Port column: the new cluster takes over 5432 and the old one is left stopped. Run the analyze pass yourself, because a freshly loaded cluster has no statistics and the first queries will be slow.

Test the application against the new cluster for a few days. Only then remove the old one:

sudo pg_dropcluster 16 main
sudo apt purge postgresql-16

The old cluster's data directory is the fastest rollback you have. Do not delete it on upgrade day.

MySQL 8.0 to 8.4: the removed option that stops the server

26.04 moves MySQL from 8.0 to 8.4 LTS, and two changes catch servers.

First, mysqld refuses to start when its config contains an option the new version removed. default_authentication_plugin is the common one, because plenty of older guides tell you to set it. The service fails, and journalctl -u mysql -n 50 names the unknown variable directly. Delete that line from the file under /etc/mysql/mysql.conf.d/, then sudo systemctl start mysql.

Second, the mysql_native_password plugin is no longer enabled by default in 8.4, so an account still using it cannot log in at all. Check while you are still on 8.0:

sudo mysql -e "SELECT user, host, plugin FROM mysql.user;"

Move every account showing mysql_native_password across before the upgrade, then update the password in your application config:

ALTER USER 'appuser'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'a new password';

If a client library is too old to speak caching_sha2_password, you can switch the old plugin back on in 8.4 by adding mysql_native_password=ON under [mysqld]. Treat that as a bridge with an end date, because the plugin is on its way out entirely.

PHP 8.3 to 8.5: your vhosts point at a socket that is gone

24.04 ships PHP 8.3 and 26.04 ships PHP 8.5. The packages install into versioned paths and nothing rewrites your web server config. An nginx vhost holding fastcgi_pass unix:/run/php/php8.3-fpm.sock; now points at a socket no process creates, so every PHP request returns 502 and the nginx error log says:

connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)

Point it at the new socket, test the config, reload:

sudo sed -i 's/php8.3-fpm.sock/php8.5-fpm.sock/' /etc/nginx/sites-available/example.com
sudo nginx -t
sudo systemctl reload nginx

On Apache with mod_php the symptom is different: Apache will not start at all, and sudo apache2ctl -t reports that it cannot load libphp8.3.so because the file does not exist. The enabled module is a symlink to a package that is gone.

sudo a2dismod php8.3
sudo a2enmod php8.5
sudo systemctl restart apache2

If you built the box from a LAMP stack on Ubuntu 24.04, both of those paths are worth checking, since the guide leaves you with a versioned module name and a versioned socket.

Your php.ini tuning does not travel either. memory_limit, upload_max_filesize and anything else you set live in /etc/php/8.3/, and the new tree starts from defaults. Diff the two files and copy the values across by hand. Copying the whole old file over the new one drags 8.3 defaults into an 8.5 install. Then run php -m and compare: an extension installed as php8.3-redis needs its php8.5- package, and if it came from a PPA the upgrader disabled that source and the extension is simply missing.

Certificates deserve one check of their own. Run sudo certbot renew --dry-run after the upgrade. It exercises the whole renewal path, including the web server reload hook, without touching the live certificate. A hook that calls a service name or a binary that changed fails here, in front of you, instead of silently in 60 days. Certbot with Let's Encrypt on nginx covers what those hooks should look like.

SSH: the failure that ends the session you are working in

The sshd_config prompt is where people lock themselves out. Answering Y installs the maintainer's file, which discards your PermitRootLogin, PasswordAuthentication, AllowUsers, Port and every other line you added. If your firewall allows only a custom port and the packaged config listens on 22, the next connection is refused, and the session you are sitting in is the last one you have.

Prevent it before you upgrade. /etc/ssh/sshd_config on 24.04 begins with Include /etc/ssh/sshd_config.d/*.conf, and OpenSSH keeps the first value it reads for each setting, so a drop-in included at the top wins over anything below. Move your settings into a file dpkg does not own:

sudo tee /etc/ssh/sshd_config.d/99-local.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
AllowUsers deploy
EOF
sudo chmod 644 /etc/ssh/sshd_config.d/99-local.conf
sudo sshd -t && sudo systemctl restart ssh

Once nothing in /etc/ssh/sshd_config is yours, that prompt stops mattering: either answer keeps your settings, because they live in a different file.

A custom port needs one more check, because it may not be where you think it is:

systemctl is-enabled ssh.socket

If that prints enabled, systemd owns the listening port and the Port line in sshd_config is ignored. Ubuntu has used socket activation for sshd since 22.10, and this is the reason a Port 2222 edit appears to do nothing. Set it on the socket unit instead, with sudo systemctl edit ssh.socket:

[Socket]
ListenStream=
ListenStream=2222

The empty ListenStream= is required. It clears the inherited value, and without it the socket listens on 22 as well as 2222. Apply it with sudo systemctl daemon-reload && sudo systemctl restart ssh.socket.

After the upgrade, before you close the session you are in:

sudo sshd -t
sudo systemctl status ssh.socket --no-pager
sudo ss -lntp | grep -E ':(22|2222)'

Then open a second terminal on your own machine and log in again. A working shell in that second terminal is the only proof that counts. Keep the first session open until you have it. Hardening SSH on a VPS goes through the settings worth keeping in that drop-in.

If it is already too late, your provider's web console gives you a login that does not use SSH at all. Log in there, fix the config, run sudo sshd -t, restart the service. That console is exactly why you test console access before an upgrade rather than during one.

FAQ

Why does do-release-upgrade say "No new release found" on Ubuntu 24.04?

Because /etc/update-manager/release-upgrades contains Prompt=lts on Ubuntu Server, and that setting offers the next long term support release only after its first point release exists. Ubuntu 26.04 LTS shipped on 23 April 2026, and 26.04.1 is scheduled for 27 August 2026. Until that day, a 24.04 server sees nothing. Leave the setting alone rather than switching to Prompt=normal, which would route you through the interim releases instead.

Do I have to reboot the server to finish the upgrade?

Yes. The upgrade installs a new kernel, a new C library and a new init system, and the running system keeps using the old ones until it restarts. do-release-upgrade asks to reboot at the end, and a machine left running until "later" is a machine running a mix of two releases. After it comes back, check uname -r for the new kernel and systemctl --failed for services that did not survive.

Should I upgrade in place or build a fresh 26.04 server?

Build fresh when you can. A new VPS lets you install the stack, restore the data, and test everything while the old server still serves traffic, so rollback is a DNS change rather than a restore from backup. Upgrade in place when the server holds state that is painful to move, when the provider bills per machine, or when you have a snapshot and proven console access. The in-place path is well trodden, but it is a one-way door for the hour it runs.

What happens if my SSH connection drops during the upgrade?

In a plain login shell the process receives SIGHUP and dies partway through, which leaves dpkg half configured. Start it inside tmux or screen and the process survives, so you reconnect and run tmux attach -t upgrade to pick it up. The upgrader also starts a spare SSH daemon on port 1022 as a second way in, but it does not open the firewall for that port, so allow 1022 yourself first and close it afterwards.

My PHP site returns 502 after upgrading. What broke?

The PHP FPM socket path changed with the version. Ubuntu 24.04 runs PHP 8.3 and 26.04 runs PHP 8.5, so /run/php/php8.3-fpm.sock no longer exists while your nginx vhost still names it. The nginx error log shows connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory). Update fastcgi_pass to the 8.5 socket, run sudo nginx -t, then reload nginx. On Apache with mod_php the equivalent fix is sudo a2dismod php8.3 followed by sudo a2enmod php8.5 and a restart.