Upgrade Ubuntu 24.04 to 26.04 on a VPS
Since 29 September 2026, Ubuntu 24.04 servers are offered the 26.04.1 upgrade. 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 now. Since 29 September 2026, when Canonical enabled upgrades from 24.04 LTS, do-release-upgrade on a 24.04 server offers 26.04.1. Ubuntu 26.04 LTS (Resolute Raccoon) was released on 23 April 2026, but Canonical only opens the LTS to LTS upgrade path once the first point release is out, because that release rolls up the install and upgrade bugs found in the first months. This time the wait ran past the point release itself: 26.04.1 shipped on 27 August 2026, and Canonical held the upgrade back for a few more weeks for planned backports fixing regressions in a recent version of rust-coreutils. If the numbering is new to you, 26.04.1 is not a different Ubuntu but the same 26.04 with four months of fixes folded into the media, which is exactly why it is the first version Canonical will hand to an existing server.
Until 29 September, running the check on a 24.04 box gave you this:
sudo do-release-upgradeChecking for a new Ubuntu release
No new release found.That was not a fault on the server, and if you still see it today, check the setting it depends on. /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 Canonical has opened the path to it. Leave it on lts anyway, because on a later release normal would also offer the short-lived interim versions. During the wait Canonical offered do-release-upgrade -d to anyone who would not hold off; now that the path is open, the plain command is the one its announcement names for servers. Read what 26.04.1 changed and when the upgrade path opened before you start. A server still on 22.04 cannot make this jump directly, because the upgrader only ever offers the next LTS, so upgrading 22.04 to 26.04 by way of 24.04 covers the first hop and the snapshot to take before the second.
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
resoluteyet. - 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 one way to back up a running database.
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 locks for a consistent dump. 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 showholddpkg --audit printing nothing means no package is half configured. apt-mark showhold printing nothing means no package carries a dpkg hold. It does not show APT preference pins, which are a separate mechanism and can also hold a package back; Step 3 checks those. Release anything showhold 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 rebootThen 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 / /bootOld 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
By default, do-release-upgrade comments out every apt source that is not an Ubuntu one, because a package built for noble can break a resolute system. Its --allow-third-party option keeps them enabled instead, which carries exactly that risk. 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 a default 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. A file in /etc/apt/preferences.d/ is read as a pin only if its name has no extension or ends in .pref and contains only letters, digits, hyphens, underscores and periods; APT ignores any other file, usually with a notice. A pin that names noble can keep a package on an old version after the upgrade, depending on which versions are still available and the priority the pin sets, so read each file and decide whether it still belongs.
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.Docker now publishes a resolute suite. Leave any source whose suite is missing 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
The server upgrader normally runs inside GNU screen and supports reattachment after a dropped connection, so losing SSH does not by itself kill the upgrade. Run it inside your own tmux session as well, so you have a named session to reconnect to.
sudo apt install -y tmux
tmux new -s upgradeInside that session:
sudo ufw allow 1022/tcp
sudo do-release-upgradeThe 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. If it did not, and you come back to a half-configured dpkg or apt sources that are part noble and part resolute, recovering a failed release upgrade covers repairing the package state and knowing when to stop repairing and restore the snapshot instead.
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 or a script changed, and pressing enter keeps the current version by default.
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 decide whether to keep your version with N or install the maintainer's version. The default is already N, but keeping the old file is not safe for every package: some configuration changes are required for the upgraded system to work.
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.
Since Ubuntu 24.04, needrestart restarts affected services automatically by default. If you restored its interactive mode, accept the full list. A daemon still using an old shared library remains exposed to bugs fixed by the update until it restarts.
Step 6: reboot, then check the machine
sudo rebootWhen 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 autoremovelsb_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 refreshes the package indexes, and apt full-upgrade then installs any updates published since the upgrade fetched its packages.
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 main cluster on server package installation only when no clusters exist yet, so an existing PostgreSQL 16 cluster does not automatically acquire an empty 18 cluster on port 5433. Your application carries on talking to 5432 and nothing looks wrong, which is why people find this months later.
pg_lsclustersAn entry for version 16 means that cluster is still on 16. If pg_lsclusters also lists an 18 main cluster, confirm it is empty before removing it with sudo pg_dropcluster --stop 18 main, because pg_upgradecluster will not write into a target cluster that already exists. Do the migration when you can stop the application:
sudo pg_upgradecluster 16 main
pg_lsclusters
sudo -u postgres vacuumdb --all --analyze-onlyThe 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. The packaged upgrade hook runs an analyze pass automatically; the command above repeats it explicitly.
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-16The 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 MySQL removed the plugin in 9.0.
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 nginxOn 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 apache2If 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 --run-deploy-hooks 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 at the next renewal. 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 for most settings OpenSSH keeps the first value it reads, so a drop-in included at the top wins over the same setting further down. A few keywords are exceptions: Port and ListenAddress accept multiple values, so a Port line in a drop-in adds a port rather than replacing one set below it. 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 sshOnce 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.socketIf that prints enabled, systemd opens the listening port, not sshd. Ubuntu has used socket activation for sshd since 22.10. On 24.04 a systemd generator reads the Port and ListenAddress settings from the sshd configuration and turns them into the socket's listen addresses, but only when systemd reloads its units. That is the reason a Port 2222 edit appears to do nothing: it takes effect after sudo systemctl daemon-reload && sudo systemctl restart ssh.socket, and the packaged sshd_config now carries a note saying so. To set the port on the socket unit directly instead, use sudo systemctl edit ssh.socket:
[Socket]
ListenStream=
ListenStream=2222The 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?
Since 29 September 2026 it should not, because that is the day Canonical enabled upgrades from 24.04 LTS to 26.04.1. Before then the message was expected: Ubuntu 26.04 LTS shipped on 23 April 2026, 26.04.1 followed on 27 August, and the path stayed closed for a few more weeks while Canonical backported fixes for rust-coreutils regressions. If you still see it, check that /etc/update-manager/release-upgrades contains Prompt=lts, the Ubuntu Server default, which offers only the next long term support release. Leave it on lts.
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?
The server upgrader normally uses GNU screen, so a dropped SSH connection does not by itself kill the upgrade. If you started it inside the named tmux session above, 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.