Upgrade MySQL 8.0 to 8.4 in Docker Compose safely

To upgrade MySQL 8.0 to 8.4 in Docker, back up the database, change the image to a pinned mysql:8.4.x tag and replace the removed --default-authentication-plugin flag in the same edit, then run docker compose up -d and let the server upgrade the data directory on its first start. The upgrade is one-way: an 8.4 data directory will not start on 8.0 again, so the backup is your only rollback.
default_authentication_plugin, so an old command: line makes the container exit at once. 8.4 also disables mysql_native_password by default, so accounts created on 8.0 with that plugin cannot log in until you enable it with --mysql-native-password=ON or move them to caching_sha2_password. Take a mysqldump and a copy of the stopped volume first, and test the restore.Why upgrade from MySQL 8.0 to 8.4 now?
Because MySQL 8.0 is no longer maintained: Oracle states that MySQL 8.0 reached end of life in April 2026, with 8.0.46 as the last bugfix release. An 8.0 container keeps running, but it gets no more security fixes, and every new CVE in the server stays open on your machine.
MySQL 8.4 is the long-term support (LTS) series that follows 8.0. The official upgrade paths list "8.0 to 8.4.x LTS" as supported by an in-place upgrade, by a logical dump and load, or by replication. For a single Docker container the in-place upgrade is the simple choice: you start the new image on the old data volume, and the server upgrades the data dictionary and system tables itself. Since MySQL 8.0.16 the server does this on its own, so no separate mysql_upgrade run is needed.
Two things on Docker Hub make the move urgent for self-hosters. First, the official mysql image no longer lists an 8.0 tag among its supported tags; 8.4 and 8 now both point to 8.4.13. Second, the floating tags moved on: lts points to 9.7 and latest to the 26.x innovation series. A compose file with an unpinned image: mysql or mysql:lts can jump over 8.4 entirely on the next docker compose pull, into a version where mysql_native_password no longer exists.
⚠️ Warning: Always pin the MySQL image to a series or a patch version (mysql:8.4ormysql:8.4.13). A major version change should be a decision you make with a backup in hand, never a side effect of pulling images.
What breaks when you change the image to mysql:8.4?
The most common failure is that the container exits immediately because its command: still passes --default-authentication-plugin=mysql_native_password, a server option that 8.4 removed. Many compose files written for MySQL 8.0 contain exactly this line:
services:
db:
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
- dbdata:/var/lib/mysql
Started with mysql:8.4, that service stops before it opens the data directory, and the log shows:
$ docker compose logs db
db | [ERROR] [MY-000067] [Server] unknown variable 'default-authentication-plugin=mysql_native_password'.
db | [ERROR] [MY-010119] [Server] Aborting
According to What Is New in MySQL 8.4, setting a removed option is an error, not a warning. The good news: the server aborts before it touches your data, so nothing has been upgraded yet. The same applies to a my.cnf mounted into /etc/mysql/conf.d/; check it as well as the command: line.
Removed options and their replacements
| Removed in 8.4 | Use instead | Typical place in a compose setup |
|---|---|---|
--default-authentication-plugin | --authentication-policy | command: |
--skip-host-cache | --host-cache-size=0 | command:, older official image setups |
expire_logs_days | binlog_expire_logs_seconds | mounted my.cnf |
--ssl, have_ssl | --tls-version | command:, my.cnf |
--no-dd-upgrade | --upgrade=NONE | rarely used |
As of November 2026. Source: MySQL 8.4 Reference Manual, What Is New.
Changes that start fine but behave differently
Some changes don't stop the server and only show up later:
- Authentication.
mysql_native_passwordis disabled by default in 8.4. An application user created on 8.0 with that plugin getsERROR 1045 (28000): Access denied, and creating such a user fails withERROR 1524 (HY000): Plugin 'mysql_native_password' is not loaded. The next section covers both ways out. - Nonstandard foreign keys. Foreign keys that reference a non-unique or partial key are now rejected in new
CREATE TABLEstatements whilerestrict_fk_on_non_standard_keyisON(the default). Existing tables upgrade fine; the server logs a warning for each such key. Schema migrations that create new ones will fail. - New reserved words.
MANUAL,PARALLEL,QUALIFYandTABLESAMPLEbecame reserved keywords. A column namedmanualmust be quoted with backticks in raw SQL; ORMs that always quote identifiers are not affected. - InnoDB defaults. Several InnoDB defaults changed, which matters on small VPS instances with slow disks. If you relied on the old values, set them explicitly.
| Variable | 8.0 default | 8.4 default |
|---|---|---|
innodb_io_capacity (I/O operations/s) | 200 | 10000 |
innodb_log_buffer_size (MiB) | 16 | 64 |
innodb_change_buffering | all | none |
innodb_adaptive_hash_index | ON | OFF |
innodb_flush_method (Linux) | fsync | O_DIRECT if supported |
temptable_max_ram (GiB) | 1 | 3% of RAM, between 1 and 4 |
As of November 2026. Source: MySQL 8.4 Reference Manual, What Is New.
How do you back up a MySQL container before upgrading?
Take two backups: a logical dump with mysqldump, which you can load into any MySQL version, and a copy of the stopped data volume, which is the fastest way back to exactly the state you had on 8.0. Then prove that the dump restores.
The reason for two copies is the one-way nature of the upgrade. The MySQL downgrade documentation supports going from 8.4 back to 8.0 only by a logical dump and load (or replication), and only for rollback purposes. An 8.4 data directory cannot simply be opened by an 8.0 server.
Before the backup, run the upgrade checker. It reads the running 8.0 server and lists removed options, authentication problems, reserved words used as identifiers and other incompatibilities. MySQL Shell is included in the mysql:8.4 image, so you don't need to install anything; when no --target-version is given, the checker targets the Shell's own release:
# find the network of your compose project, e.g. myapp_default
docker network ls
# run the 8.4 upgrade checker (MySQL Shell ships in the mysql:8.4 image)
# against the 8.0 server that is still running
docker run --rm -it --network myapp_default mysql:8.4.13 \
mysqlsh -- util checkForServerUpgrade root@db:3306
Read every Error in the report; warnings about mysql_native_password are expected and are handled below. Then stop the writers and take the backups. Replace myapp_ with your compose project name and the service names with your own:
# 1. stop everything that writes to the database (app, workers, scheduler)
docker compose stop app queue scheduler
# 2. logical dump of the application database
docker compose exec -T db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--single-transaction --routines --triggers --events \
--databases "$MYSQL_DATABASE"' | gzip > db-before-8.4.sql.gz
# 3. physical copy of the stopped data volume
docker compose stop db
docker volume ls | grep dbdata # e.g. myapp_dbdata
docker run --rm -v myapp_dbdata:/var/lib/mysql:ro -v "$PWD":/backup alpine \
tar czf /backup/dbdata-8.0.tgz -C /var/lib/mysql .
The dump uses --single-transaction, which gives a consistent snapshot of InnoDB tables without locking them, and includes stored routines, triggers and events. The volume copy is taken only after docker compose stop db: copying the files of a running MySQL server gives you a data directory that may not start. MySQL's own upgrade preparation guide also asks for a clean shutdown before an upgrade, which docker compose stop does as long as the server finishes within the stop timeout (10 seconds by default; raise it with stop_grace_period for large buffer pools).
Test the dump on a throwaway 8.0 container, so you know the file is complete before you need it:
docker run -d --name restore-test -e MYSQL_ROOT_PASSWORD=test mysql:8.0
docker logs -f restore-test # wait for "ready for connections", then Ctrl+C
gunzip -c db-before-8.4.sql.gz | docker exec -i restore-test mysql -uroot -ptest
docker exec restore-test mysql -uroot -ptest -e \
"SELECT table_schema, COUNT(*) AS tables FROM information_schema.tables GROUP BY table_schema;"
docker rm -f restore-test
Copy both files off the server, or at least to a different disk. If you already run scheduled backups, our backup and restore guide describes how to keep and test them.
📋 Checklist before you change the image
- Upgrade checker run, no unexplained errors
-
mysqldumpfile created and restored on a test container - Stopped volume copied to a
.tgz - Free disk space for the upgrade and the backups
- Old
docker-compose.ymlkept as a copy
What should you do about mysql_native_password?
Either keep it on for now with --mysql-native-password=ON, or move every account to caching_sha2_password before or right after the upgrade. Both are valid on 8.4; only the second works on MySQL 9, where the plugin is removed.
First find out which accounts use which plugin. Run this on the 8.0 server before the upgrade:
SELECT user, host, plugin
FROM mysql.user
ORDER BY plugin, user;
If every application account already shows caching_sha2_password, which is the default for new accounts since MySQL 8.0, you don't need the native plugin at all: remove the old flag, keep the default authentication_policy of *,,, and you are done. Accounts that show mysql_native_password usually exist because the compose file forced that plugin when the database was first created, often for an old PHP driver.
✅ Keep mysql_native_password on
- No change to users or passwords; the upgrade is only a compose edit
- Works with clients that cannot speak
caching_sha2_password, such as older client libraries and some builds of the MariaDB client tools - Can be switched off later, account by account
❌ Considerations
- The plugin is deprecated since 8.0.34 and removed in MySQL 9.0, so this only postpones the work
- Its SHA-1 based hashing is weaker than SHA-256 based
caching_sha2_password - The upgrade checker and the server log keep warning about it
Option A: keep native authentication
Add --mysql-native-password=ON to the server flags. If new accounts should also use the native plugin (for example because your app's installer creates its own user), replace the removed option with --authentication-policy=mysql_native_password,,. The authentication_policy reference explains the format: up to three comma-separated factors, where the first element sets the plugin for the first factor and the empty elements leave the optional second and third factors open. A softer form, *:mysql_native_password,,, makes native the default while still allowing other plugins.
Option B: move accounts to caching_sha2_password
Start 8.4 with the native plugin on (option A) so the accounts can still log in, then change them one at a time and restart without the flag once none is left:
-- move one account to the 8.4 default plugin (keep or change the password)
ALTER USER 'app_user'@'%' IDENTIFIED WITH caching_sha2_password BY 'the-password';
-- check the result
SELECT user, host, plugin FROM mysql.user WHERE user = 'app_user';
Check that every client can authenticate this way before you switch the plugin off. Current PHP (mysqlnd), Python and Node.js MySQL drivers support caching_sha2_password. The first login of a new session needs either a TLS connection or RSA key exchange; MySQL generates TLS certificates on first start, so connections between containers normally use TLS without any configuration. Backup tools are the usual surprise: if your backup container uses the MariaDB mysqldump, try a real dump with the changed account before you remove the native plugin.
How do you upgrade MySQL 8.0 to 8.4 in Docker Compose, step by step?
Change the image and the server flags in one edit, start only the database, wait for the data dictionary upgrade to finish in the log, then start the rest of the stack.
- Edit the db service
Pin
mysql:8.4.13(ormysql:8.4if you want patch updates on every pull) and replace the old flag. The example keeps native authentication, which is the safe default for an existing database; drop the two auth flags if all your accounts already usecaching_sha2_password.services: db: image: mysql:8.4.13 command: >- --mysql-native-password=ON --authentication-policy=mysql_native_password,, --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - dbdata:/var/lib/mysql - Pull the image and start the database alone
Starting
dbon its own keeps the application from connecting while the server upgrades its system tables.docker compose pull db docker compose up -d db docker compose logs -f db - Watch the upgrade in the log
On the first start the server notices the 8.0 data directory, upgrades the data dictionary and the system schemas, and then opens for connections. Expect lines like these (abridged; your version numbers will differ):
[System] [MY-013381] [Server] Server upgrade from '80046' to '80413' started. [System] [MY-013381] [Server] Server upgrade from '80046' to '80413' completed. [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.4.13' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.The server upgrades metadata and system tables, not the rows in your tables, so for a typical application database this is a short pause. Don't stop the container while the upgrade is running. If the log shows an error instead, MySQL's upgrade documentation says the server reverts its changes to the data directory, so fix the reported problem and start again.
- Start the application
Run
docker compose up -dfor the whole project. If the app waits for the database with a healthcheck (mysqladmin pingworks unchanged on 8.4), it starts by itself oncedbis healthy. - Keep the old compose file and the backups
Don't delete the
.tgzand the dump for at least a few days of normal use. They are the only way back.
ℹ️ Note:docker compose pullnever edits your compose file. If an application release ships a newdocker-compose.ymlwithmysql:8.4, you still have to copy that file (or merge thedbservice) into your install before anything changes.
How do you check that the upgrade worked?
Check four things: the server reports 8.4, the character set and collations are unchanged, the application logs in and sees all its migrations, and your backup command still produces a dump.
# server version and character set
docker compose exec db mysql -uroot -p -e \
"SELECT VERSION(); SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'collation_server';"
# table collations per schema (compare with the same query before the upgrade)
docker compose exec db mysql -uroot -p -e \
"SELECT table_schema, table_collation, COUNT(*) FROM information_schema.tables
WHERE table_schema NOT IN ('mysql','sys','information_schema','performance_schema')
GROUP BY 1, 2;"
# the application still sees all its migrations (Laravel example)
docker compose exec app php artisan migrate:status
# a dump still works with the client your backups use
docker compose exec -T db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" \
--no-data --databases "$MYSQL_DATABASE"' > /dev/null && echo "dump OK"
Compare the collation query with its output from before the upgrade. An in-place upgrade keeps existing tables as they are; a difference usually means a table was recreated by a migration under different server defaults. Keep the --character-set-server and --collation-server flags you used on 8.0, so new tables match the old ones.
Then use the application: log in, save something, run a background job and trigger one scheduled backup. Look at the MySQL log once more for warnings about nonstandard foreign keys or deprecated options, and fix them while you still remember what you changed.
📌 Key takeaways
- Change the image and the
command:together; the old auth flag makes 8.4 exit at once - The data dictionary upgrade runs by itself on the first start; watch the log for "completed"
- An account created with
mysql_native_passwordneeds the plugin enabled or a move tocaching_sha2_password - Verify with real use and a real backup, not only with
SELECT VERSION()
Can you roll back from 8.4 to 8.0?
Only by restoring a backup. MySQL does not support an in-place downgrade from 8.4 to 8.0; the downgrade table allows only a logical dump and load or replication, and only for rollback when no new 8.4 functionality has been used on the data.
In practice there are two ways back. If the upgrade failed or the application misbehaves within minutes, restore the stopped-volume copy: you get exactly the 8.0 state, but you lose everything written after the backup. If 8.4 has been running for a while and you have new data you want to keep, take a fresh mysqldump on 8.4 and load it into a new, empty 8.0 container, which is the supported path.
docker compose stop
docker compose rm -f db
docker volume rm myapp_dbdata # deletes the upgraded 8.4 data
# put the old docker-compose.yml back (image: mysql:8.0 and the old command:)
docker compose create db # recreates the empty volume
docker run --rm -v myapp_dbdata:/var/lib/mysql -v "$PWD":/backup alpine \
tar xzf /backup/dbdata-8.0.tgz -C /var/lib/mysql
docker compose up -d
⚠️ Warning:docker volume rmdeletes the data for good. Run it only after you have checked thatdbdata-8.0.tgzexists, is not empty and lists files when you runtar tzf dbdata-8.0.tgz | head.
Keep in mind that rolling back puts you on an unsupported version again. Treat it as a way to buy time while you fix what blocked the upgrade, not as a destination.
Upgrading the database of a NetSendo install
NetSendo moved its docker-compose.yml to MySQL 8.4 LTS in version 2.2.0 (6 November 2026). The db service now uses image: mysql:8.4 with --mysql-native-password=ON --authentication-policy=mysql_native_password,,, so the database user that an 8.0 install created keeps logging in, and the MariaDB mysqldump in the app image, which NetSendo's backups use, can still authenticate.
An existing install does not change by itself. Pulling a new app image leaves your compose file as it is, so the database stays on 8.0 and keeps working with the new image. To upgrade, take a database backup, replace docker-compose.yml with the one from the release (image and command: together), and run docker compose up -d; MySQL upgrades the data directory on its first start. The release notes describe the tested path: migrations on 8.0, the same volume started on 8.4, the app logging in and mysqldump working. The general installation steps are in the quickstart, and the database settings in .env are explained in the configuration docs.
If you are still choosing a platform to host yourself, the comparison of self-hosted email marketing software covers the database each tool needs and what running it involves.
🎯 Expert tips
Restore the volume copy into a second compose project on the same host, upgrade that one, and point a staging instance of the app at it. Ten minutes of rehearsal shows every problem the real upgrade would hit.
mysql:8.4.13 changes only when you edit it. Raise it on purpose, after reading the release notes, rather than on whatever day someone runs docker compose pull.
Keeping it on is fine for 8.4, but MySQL 9 removes it. Moving one account at a time while 8.4 is stable is far easier than doing it in the middle of the next major upgrade.
Set stop_grace_period: 1m on the db service. A MySQL server killed after the default 10 seconds may need crash recovery, and an unclean shutdown is exactly what you don't want right before an upgrade.
Frequently asked questions
Can I upgrade MySQL 8.0 directly to 8.4?
Yes. MySQL documents 8.0 to 8.4 LTS as a supported path by in-place upgrade, dump and load, or replication. From 5.7 you have to go through 8.0 first.
Why does my MySQL 8.4 container exit with "unknown variable 'default-authentication-plugin'"?
MySQL 8.4 removed the default_authentication_plugin option, and setting a removed option stops the server. Delete the flag from command: or my.cnf and use --authentication-policy, plus --mysql-native-password=ON if your accounts still use that plugin.
Is mysql_native_password still available in MySQL 8.4?
Yes, but it is disabled by default and must be enabled with --mysql-native-password=ON. It is deprecated and removed in MySQL 9.0, so plan to move accounts to caching_sha2_password.
Can I downgrade from MySQL 8.4 back to 8.0?
Not in place. MySQL supports going back only by a logical dump and load or by replication, for rollback purposes, so in practice you restore the backup taken before the upgrade.
Should I skip 8.4 and go straight to MySQL 9.7 LTS?
You can, but MySQL 9 no longer has mysql_native_password, so every account must use caching_sha2_password first. On Docker Hub the lts tag points to 9.7, which is why the image should be pinned to the version you have tested.
How long does the MySQL 8.0 to 8.4 upgrade take in Docker?
The server upgrades the data dictionary and system tables on the first start, not the rows of your tables, so for a typical application database it is a short pause. Watch the container log until the upgrade is reported as completed and the server is ready for connections.
Sources
- MySQL 8.4 Reference Manual: What Is New in MySQL 8.4
- MySQL 8.4 Reference Manual: Upgrade Paths
- MySQL 8.4 Reference Manual: Downgrading MySQL
- MySQL 8.4 Reference Manual: Native Pluggable Authentication
- MySQL 8.4 Reference Manual: authentication_policy
- Oracle Cloud: MySQL Server Versions (MySQL 8.0 end of life)
- Docker Hub: official mysql image and supported tags
- NetSendo v2.2.0 release notes
Run your marketing stack on your own server
NetSendo's Docker setup already ships with MySQL 8.4 LTS and keeps your data on your own infrastructure.


