RE:NODE

Upgrade MySQL 8.0 to 8.4: checker, backup, upgrade, rollback

By RE:NODE Team
16 min read


Upgrading MySQL 8.0 to 8.4 is a four-part job, and the order matters more than any single command. Run MySQL Shell's upgrade checker against the live 8.0 server and fix every error it reports. Take a backup you can restore to 8.0 - a cold copy of the data directory for an in-place upgrade, a logical dump for everything else. Then either start the 8.4 binaries on the old data directory (in-place) or load a dump into a fresh 8.4 server (logical). Finally, test the application against 8.4 before real traffic arrives, because there is no in-place way back: once 8.4 has started on a data directory, 8.0 refuses to open it.

What changes between the versions - the authentication default, the removed statements, the new InnoDB defaults - is covered in MySQL 8.4 LTS: what changed. This guide is the procedure: the commands, what the checker's output means, the two upgrade routes in detail, how to test, and exactly how far rollback goes.

In-place or logical: pick the route first#

Both routes end with your data on 8.4. They differ in downtime, in what you need control of, and in what rollback looks like.

In-placeLogical (dump and load)
What happens8.4 binaries start on the 8.0 data directoryData is exported as SQL or files, loaded into a new 8.4 server
You needRoot on the machine running MySQLA login on both servers
DowntimeMinutes, plus data dictionary upgradeDump time plus load time, usually much longer
Speed on large dataFast - no data is rewrittenSlow - every row and index is rebuilt
Old server afterwardsGone, unless you copied the filesUntouched, still running 8.0
Clean-up bonusNoneTables are rebuilt, fragmentation and old row formats go

Use in-place when you run MySQL yourself on a VDS or your own machine and the data is large enough that a reload would take hours. Use logical when the database lives on a hosted service where someone else runs the server, when you are moving to a new machine at the same time, or when the data is small enough - a few gigabytes - that loading it takes minutes anyway. A logical upgrade also has the most comfortable rollback, because the 8.0 server is still there, unchanged, until you switch it off.

There is a third route for people who cannot afford downtime: make a new 8.4 server a replica of the 8.0 source, let it catch up, then point the application at it. Replication from an older source to a newer replica is supported; the reverse is not, so once writes land on 8.4 the old 8.0 server cannot follow. That route is beyond a small application's needs and is not covered in detail here.

Upgrade checkeragainst live 8.0Fix errors on 8.0config, accounts, schemaBackuprestored at least onceIn-place8.4 on old datadirDump and loadinto new 8.4Test the applicationbefore real traffic
The order of an 8.0 to 8.4 upgrade

Step 1: back up first, in a form 8.0 can read#

The backup taken before an upgrade has one job beyond the usual: it must be restorable to 8.0, because that is the version you would be going back to. Two kinds qualify.

A cold copy of the data directory. Stop 8.0 cleanly and copy the whole directory - usually /var/lib/mysql on Linux - somewhere else. This is the only backup that undoes an in-place upgrade quickly: put the copy back, reinstall 8.0, start it, and you are where you were. It is useless for any other purpose, because it only opens on the same major version, but for this one it is perfect. A filesystem snapshot (LVM, ZFS) of a cleanly stopped server serves the same purpose and takes seconds.

bash
$ sudo systemctl stop mysql          # mysqld on RHEL-family systems$ sudo cp -a /var/lib/mysql /var/backups/mysql-80-datadir$ sudo cp -a /etc/mysql /var/backups/mysql-80-etc

Copy the configuration too. The upgrade will make you edit it, and a rollback needs the 8.0 version back.

A logical dump. This is the backup for the logical route, and the second line of defence for the in-place one. mysqldump with --single-transaction --routines --events --triggers produces a consistent SQL file without stopping the server; mysqldump backup and restore explains every flag and the errors it hits. For larger databases, MySQL Shell's dump utility is faster because it writes in parallel:

javascript
// in mysqlsh, JavaScript mode (\js)util.dumpInstance("/var/backups/dump-80", {threads: 4})

util.dumpInstance() includes user accounts and grants by default, which mysqldump handles poorly - it dumps the mysql schema as tables, and loading those into a different version is asking for trouble. If you use mysqldump, save the accounts separately: for each application account, SHOW CREATE USER and SHOW GRANTS give you statements you can replay.

Step 2: run util.checkForServerUpgrade#

The upgrade checker lives in MySQL Shell (mysqlsh), not in the server. It connects to the running 8.0 server, inspects the configuration, the accounts, the schemas and the stored code, and reports what would fail or behave differently on the target version. It changes nothing.

You need a MySQL Shell at least as new as the target: the checker can test for any version from 8.0.11 up to its own version number, and no further. Shell 8.4 or a 9.x release both work for an 8.4 target. It can run on any machine that can reach the server, including your laptop, but the configuration file check needs the file on the machine where Shell runs, so running it on the database server itself gives the most complete report.

bash
$ mysqlsh -- util check-for-server-upgrade root@localhost:3306 \    --target-version=8.4.6 --config-path=/etc/mysql/my.cnf

The same thing from inside an interactive Shell session, in JavaScript mode:

javascript
util.checkForServerUpgrade("root@localhost:3306", {  targetVersion: "8.4.6",  configPath: "/etc/mysql/my.cnf"})

If you leave out targetVersion, the checker assumes the version matching the Shell itself - so a Shell 9.x run without it checks for 9.x and reports problems you do not have yet. Pin the 8.4 patch you are actually installing. If you leave out configPath, the checks that read the option file fail with a message asking for it; on Debian and Ubuntu the file is /etc/mysql/my.cnf (which includes mysql.conf.d/), on RHEL-family systems /etc/my.cnf.

The account running the checker needs the PROCESS and SELECT privileges. Root is the easy answer on your own server. On a hosted server where you only have an application user, grant what you can and run it anyway; checks that cannot read what they need report that rather than passing silently.

Reading the report

The text output is a numbered list of checks, each ending in No issues found, or a list of findings marked Error, Warning or Notice, followed by a summary:

code
Errors:   2Warnings: 5Notices:  02 errors were found. Please correct these issues before upgradingto avoid compatibility issues.

Errors block the upgrade or make the server refuse to start - a removed option in the configuration file is the usual one. Fix all of them on the 8.0 server, then run the checker again until the count is zero. Warnings do not stop the upgrade but describe something that will behave differently or break later; read each one. Notices are informational.

The checks most often triggered on an 8.0 server heading to 8.4:

CheckWhat it finds
sysvarRemoved options in the config file, and settings whose defaults change
authMethodUsageAccounts still on mysql_native_password
deprecatedDefaultAuthdefault_authentication_plugin still set
pluginUsagePlugins that were removed or replaced by components
foreignKeyReferencesForeign keys pointing at non-unique or partial keys
reservedKeywordsIdentifiers that became reserved words
invalidPrivilegesPrivileges that no longer exist or changed

mysqlsh -- util check-for-server-upgrade --list --target-version=8.4.6 prints every check with a description and the versions it applies to. For automation, --output-format=JSON gives a single object with errorCount, warningCount and a checksPerformed array; a CI step or a script can refuse to continue while errorCount is above zero. The checker does not signal failure through its exit code, so read the count rather than relying on the command failing.

Two findings deserve a word on how to fix them. For authMethodUsage, move each account to caching_sha2_password with ALTER USER ... IDENTIFIED WITH caching_sha2_password BY '...' - after checking that the application's driver supports it. For sysvar, edit the option file and delete or rename the removed settings; the most common are expire_logs_days (replace with binlog_expire_logs_seconds) and default_authentication_plugin. Both fixes work on 8.0, so make them there, restart, and confirm the application still runs before the upgrade itself. That separates "the config change broke something" from "8.4 broke something", which is a distinction you will want.

Step 3a: the in-place upgrade on Linux#

With the checker clean and the cold copy taken, the in-place upgrade is short. The MySQL documentation recommends a slow shutdown before an in-place upgrade between release series, so InnoDB finishes purge and change-buffer merges before the files are handed to the new version:

sql
SET GLOBAL innodb_fast_shutdown = 0;

Then stop the server and swap the packages. On Debian or Ubuntu with Oracle's APT repository, the series is chosen by the mysql-apt-config package:

bash
$ sudo systemctl stop mysql$ sudo dpkg-reconfigure mysql-apt-config   # choose the 8.4 LTS series$ sudo apt update$ sudo apt install mysql-server

On RHEL, Rocky or Alma with Oracle's yum repository, disable the 8.0 repository and enable the 8.4 LTS one - dnf repolist all | grep mysql shows their exact names in your release package - then sudo dnf upgrade mysql-community-server. Distribution-packaged MySQL follows the distribution's own schedule and may not offer 8.4 at all; if apt policy mysql-server shows only 8.0 candidates, you need Oracle's repository or a container.

In Docker the swap is a tag change. Stop the container, change image: mysql:8.0 to image: mysql:8.4, and start it on the same volume. Pin to 8.4 or a specific patch, never latest, which moved past 8.4 to Innovation releases.

What happens at first start

When 8.4 starts on an 8.0 data directory, it upgrades the data dictionary, then the mysql system schema, the sys schema and the help tables. Watch the error log while it happens:

bash
$ sudo tail -f /var/log/mysql/error.log

You are looking for a pair of lines reading Server upgrade from '80xxx' to '804xx' started and ... completed, then ready for connections. On a server with a few hundred tables this takes seconds; with tens of thousands it can take minutes. Do not kill it halfway - an interrupted dictionary upgrade is what the cold copy is for.

The behaviour is controlled by the server's --upgrade option, which defaults to AUTO (upgrade whatever needs it). --upgrade=FORCE re-runs the system table upgrade even when the server thinks it is done, which is the fix when the system schema is suspected to be half-upgraded. mysql_upgrade no longer exists; there is nothing to run by hand.

If the server will not start, the log says why in its first few error lines. The usual cause is an option the checker flagged and nobody removed - the log shows unknown variable with the name. Remove it from the configuration and start again.

Step 3b: the logical upgrade#

The logical route needs a running 8.4 server - a new machine, a new container, or a hosted MySQL 8.4 database - and the dump from step 1. With mysqldump output it is a plain import:

bash
$ gunzip -c appdb-80.sql.gz | mysql -h new-host -P 3306 -u root -p appdb

With a MySQL Shell dump, use the matching loader, which loads tables in parallel:

javascript
util.loadDump("/var/backups/dump-80", {threads: 4, loadUsers: true})

Two things trip util.loadDump(). It loads data with LOAD DATA LOCAL INFILE, so the target server must have local_infile set to ON; if it is off and you cannot change it, use mysqldump instead. And loadUsers is off by default, so accounts and grants are skipped unless you ask for them. On a hosted server where you are not a full administrator, leaving users out and creating the application accounts yourself is often cleaner - the account you have may not be allowed to create accounts with arbitrary privileges anyway.

A rehearsal is the point of the logical route. Load into the new server while the old one keeps serving traffic, test everything against the copy, and when it passes, take a fresh dump during a short write freeze and load that. Writes between the rehearsal dump and the final one are the only thing you need the freeze for. Migrating MySQL to a new host covers the cut-over itself: the freeze, the final dump, switching connection strings, keeping the old server read-only for a few days.

Definers are the classic import failure. A view, trigger or routine created by admin@localhost on the old server carries DEFINER=admin@localhost, and on 8.4 creating an object with another account as definer needs the SET_ANY_DEFINER privilege. If the import stops with an access-denied error on a CREATE VIEW or CREATE TRIGGER, either create the definer account first or strip the clauses from the dump.

Step 4: test the application before traffic arrives#

The upgrade checker inspects the database. It cannot see the application, and the application is where 8.4 surprises show up. Test against the upgraded copy - a rehearsal server for the logical route, a staging server or a copy restored from the cold backup for in-place.

What to test, roughly in order of how often it breaks:

  1. Every account can log in. Not just the main application user - the read-only reporting account, the backup user, the one the cron job uses. An account still on mysql_native_password fails here and nowhere else.
  2. Every client and driver connects. Old connectors are the usual failure: a driver that cannot do caching_sha2_password reports that the plugin cannot be loaded. Check language drivers, GUI tools, and any backup or monitoring agent. MySQL remote connections covers the TLS behaviour of that plugin.
  3. The test suite passes. Run the application's own tests against 8.4. Migrations deserve special attention: an ORM that creates a foreign key on a non-unique column fails on 8.4 even though the existing ones survived the upgrade.
  4. Scheduled jobs and scripts run. Anything that issues SHOW SLAVE STATUS, FLUSH HOSTS or RESET MASTER now fails with a syntax error. Search your repositories and crontabs for them.
  5. The slow queries stay fast. The default sql_mode is unchanged from 8.0, so query results should be identical, but plans can shift with the new InnoDB defaults and optimizer changes. Take the top statements from performance_schema.events_statements_summary_by_digest on 8.0, run EXPLAIN on each on both versions, and compare. MySQL indexes and EXPLAIN explains how to read the differences, and the slow query log catches what you missed once real traffic arrives.

Watch memory on a small server for the first day. 8.4's temptable_max_ram default is 3% of memory but never below 1 GB, which on a 1 or 2 GB machine can let temporary tables use far more than intended. If you did not set it on 8.0, set it explicitly now.

Rollback: what you can and cannot undo#

This is the part people learn the hard way, so it is worth being exact.

An in-place downgrade from 8.4 to 8.0 is not supported. Once 8.4 has started on a data directory, the data dictionary carries the new version. Start 8.0 on it and InnoDB refuses with Downgrade is not supported. There is no tool that converts it back. The only fast way back is the cold copy from step 1: stop 8.4, move its data directory aside, restore the copy, reinstall the 8.0 packages and the old configuration, start. Everything written after the copy was taken is lost, which is why the window between taking it and deciding to keep 8.4 should be short.

A logical dump from 8.4 into 8.0 is supported, for rollback only. The 8.4 manual lists dump and load (or replication) as the way from 8.4 back to 8.0, on the condition that no new 8.4 functionality has been used on the data. This is how you keep writes made after the upgrade: dump the 8.4 server and load it into 8.0. It is slow on large data, and it works only while the schema uses nothing 8.0 lacks - a new privilege in a grant, a GTID tag, a feature introduced in 8.1 to 8.4. The longer you run on 8.4, the more likely something has crept in.

The logical route rolls back by switching the connection string. The 8.0 server is still running. If you kept it read-only during the cut-over, switching back is instant, minus whatever was written to 8.4 in the meantime.

RouteWay backWrites after the upgrade
In-placeRestore cold copy, reinstall 8.0Lost
In-placeDump 8.4, load into 8.0Kept, if no 8.4-only features used
LogicalPoint the app back at 8.0Lost unless dumped across

Decide your point of no return in advance. A sensible one: keep the cold copy or the old server for a week, and do not adopt anything 8.4-specific in that time. After that, roll forward rather than back - fix problems on 8.4, since 8.0 no longer receives security fixes.

Troubleshooting the upgrade#

Most failures fall into a handful of patterns, and the error log or client message names them.

The server will not start after the package swap: read the first [ERROR] lines in the log. unknown variable means a removed option in a config file - check included directories, not just the main file. A message about the data directory being from a newer version means you are starting 8.0 on an upgraded directory; restore the cold copy.

The application cannot log in but mysql can: the account is on mysql_native_password, or the driver is too old for caching_sha2_password. As a stopgap, mysql_native_password=ON in the [mysqld] section re-enables the old plugin, but it is gone in 9.0, so treat that as days of grace rather than a fix.

The checker reports nothing but the import fails: the checker inspects the source server, not your dump file. Definer errors, SET @@GLOBAL.GTID_PURGED lines and local_infile refusals are load-time problems; the mysqldump guide covers each.

Queries are slower after an in-place upgrade: run ANALYZE TABLE on the busiest tables so statistics are fresh, then compare plans. If one workload relied on the adaptive hash index, innodb_adaptive_hash_index=ON brings back the 8.0 behaviour; measure before and after rather than flipping it on by default.

How this works on RE:NODE#

The MySQL line on RE:NODE already runs MySQL 8.4 LTS, so moving there from an 8.0 server elsewhere is the logical route: run the checker against your old server, dump, and load into the new one with the root password and application database generated for it. You do not run the server binaries, so there is no in-place upgrade to do, and the panel's backups - on demand or scheduled, restored with a button and stored off the machine - cover the database after it lands. Test against the new server before switching the application's connection string, and keep the old 8.0 server untouched until you are satisfied.

FAQ#

Can I upgrade from MySQL 5.7 straight to 8.4?

Not in place. 5.7 must go to 8.0 first, then 8.0 to 8.4. A logical dump from 5.7 usually loads into 8.4 directly, with the exceptions being reserved words, zero dates under strict mode and old authentication plugins, so run the checker against the 5.7 server with an 8.4 target before trying.

Does the upgrade checker change anything on my server?

No. It only reads: configuration, metadata from information_schema and the mysql schema, and stored code. It opens its own session and leaves the Shell's current session alone. It is safe to run against production during working hours.

How long does an in-place upgrade take?

The package swap takes a minute or two. The data dictionary upgrade at first start depends on the number of tables, not the amount of data - seconds for an ordinary application, minutes for one with tens of thousands of tables. A slow shutdown beforehand can add time on a busy server with a lot of pending purge.

Should I go to 8.4 or straight to a 9.x release?

8.4 for a production database. It is the long-term support series and gets fixes without behaviour changes. The 9.x Innovation releases are supported only until the next one and keep removing things, including mysql_native_password entirely.

Do I have to upgrade the application's MySQL driver?

Only if it cannot handle caching_sha2_password, which is true of the old Node mysql package and of drivers from the 5.7 era. Drivers from the last several years handle it. Check by connecting to the upgraded copy with the exact driver version the application ships with.