Step 1 — Verify Ubuntu and the MySQL package candidate
Confirm the operating system and architecture:
cat /etc/os-release
uname -m
Refresh package metadata:
Inspect the package candidate before installing:
apt-cache policy mysql-server mysql-server-8.0
On Ubuntu 24.04, the mysql-server metapackage resolves to the MySQL 8.0 line. At this revision, the current Ubuntu package is 8.0.46.
Check CPU, memory, and disk headroom:
Do not copy a fixed production VM size from a tutorial. MySQL memory use depends on the buffer pool, connections, per-session buffers, temporary tables, query shape, and other processes on the server.
Verify: Ubuntu reports 24.04, APT exposes a MySQL 8.0 candidate from Ubuntu repositories, and the VM has enough free storage for the database plus backup growth.
Step 2 — Install MySQL Server from Ubuntu
Install MySQL Server and UFW:
sudo apt install -y mysql-server ufw
Verify the client and server versions:
mysql --version
sudo mysql -NBe 'SELECT VERSION();'
The exact Ubuntu package revision changes as security and maintenance updates are published.
Confirm Ubuntu still owns the installed package path:
apt-cache policy mysql-server-8.0
Do not add Oracle's separate MySQL APT repository after installation unless you have a tested repository-migration and rollback plan.
Verify: mysql-server installs successfully, SELECT VERSION() returns MySQL 8.0.x, and APT identifies Ubuntu's repository as the package source.
Step 3 — Verify the service and port 3306
Confirm MySQL is active and enabled:
systemctl is-active mysql
systemctl is-enabled mysql
sudo systemctl status mysql --no-pager
Check the server through its local Unix socket:
Expected output:
Inspect listeners:
sudo ss -lntp | grep ':3306' || true
Read the active bind address and port from MySQL:
sudo mysql -NBe 'SELECT @@bind_address, @@port;'
Verify: the MySQL service is active and enabled, mysqladmin ping succeeds, and no unexpected public listener exists on TCP 3306.
Step 4 — Verify local root administration
Open the administrative client through sudo:
Inspect the local root account instead of assuming its authentication method:
SELECT user, host, plugin
FROM mysql.user
WHERE user = 'root';
Keep the administrative account restricted to localhost. MySQL supports socket peer-credential authentication for local administration, while application accounts should use their own credentials.
Exit:
Do not convert the root account into a remotely accessible application account. Create a separate least-privileged application user in Step 6.
Verify: sudo mysql provides local administrative access, every root account is restricted to localhost, and no remote root path is required by the application.
Step 5 — Run mysql_secure_installation
Run MySQL's hardening utility:
sudo mysql_secure_installation
Prompts can vary by package revision and account state. The important final state is:
- no anonymous MySQL users;
- no remotely accessible root account;
- no
test database;
- password-strength validation enabled only when it fits your account policy.
Verify the state directly:
sudo mysql -NBe \
"SELECT user, host FROM mysql.user WHERE user = '';"
sudo mysql -NBe \
"SELECT user, host FROM mysql.user WHERE user = 'root';"
sudo mysql -NBe \
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'test';"
The anonymous-user and test-database queries return no rows. Root remains local.
Verify: anonymous users and the test database are absent, and no root account is available for remote application access.
Step 6 — Create an application database and user
Open MySQL as the local administrator:
Create a dedicated database:
CREATE DATABASE raffapp
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
Create a local application account with a generated password:
CREATE USER 'raffappuser'@'localhost'
IDENTIFIED WITH caching_sha2_password BY RANDOM PASSWORD;
Store the generated password immediately in your secrets manager. It cannot be recovered later in plain text.
Grant database-scoped privileges:
GRANT ALL PRIVILEGES ON raffapp.*
TO 'raffappuser'@'localhost';
Inspect the account and grants:
SHOW CREATE USER 'raffappuser'@'localhost';
SHOW GRANTS FOR 'raffappuser'@'localhost';
EXIT;
MySQL 8.0 uses caching_sha2_password as its default authentication plugin. Do not downgrade an application account to a legacy plugin to accommodate an obsolete connector.
Verify: raffappuser uses caching_sha2_password and has privileges on raffapp.* rather than global administrative privileges.
Step 7 — Run an authenticated CRUD test
Connect as the application user:
mysql -u raffappuser -p raffapp
Create a disposable table:
CREATE TABLE tutorial_check (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY unique_name (name)
) ENGINE=InnoDB;
Insert, read, update, and delete a row:
INSERT INTO tutorial_check (name, status)
VALUES ('mysql-tutorial-test', 'created');
SELECT name, status
FROM tutorial_check
WHERE name = 'mysql-tutorial-test';
UPDATE tutorial_check
SET status = 'verified'
WHERE name = 'mysql-tutorial-test';
SELECT name, status
FROM tutorial_check
WHERE name = 'mysql-tutorial-test';
DELETE FROM tutorial_check
WHERE name = 'mysql-tutorial-test';
DROP TABLE tutorial_check;
EXIT;
The second SELECT returns verified.
Verify: the non-root application account creates, reads, updates, and deletes data inside its own database without global administrative privileges.
Step 8 — Keep MySQL off the public network
Inspect the bind address and active listener:
sudo mysql -NBe 'SELECT @@bind_address, @@port;'
sudo ss -lntp | grep ':3306' || true
For an application on the same VM, keep MySQL local. Do not change the listener to 0.0.0.0 simply because a GUI or another tutorial expects public TCP access.
Before changing UFW, open a second SSH session and inspect the current rules:
If UFW is already active, add a defense-in-depth deny only when it does not conflict with an intentional private-network rule:
If UFW is inactive, follow Set Up UFW Firewall on Ubuntu 24.04 rather than enabling it blindly from one remote shell.
Verify: MySQL does not listen on a public interface for the single-VM design and no public UFW rule permits TCP 3306.
Step 9 — Allow one private app server with TLS
Skip this step when the application and database share the same VM. For a separate application VM on Raff VPC, replace these example addresses with the real private IPs:
Database private IP: 10.0.0.5
Application private IP: 10.0.0.10
Back up the server configuration:
sudo cp /etc/mysql/mysql.conf.d/mysqld.cnf \
/etc/mysql/mysql.conf.d/mysqld.cnf.before-private-access
Edit the configuration:
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
Bind MySQL only to loopback plus the database VM's private address:
bind-address = 127.0.0.1,10.0.0.5
Validate and restart:
sudo mysqld --validate-config
sudo systemctl restart mysql
systemctl is-active mysql
Create an account restricted to the application's exact private IP and require encrypted transport:
CREATE USER 'raffappuser'@'10.0.0.10'
IDENTIFIED WITH caching_sha2_password BY RANDOM PASSWORD
REQUIRE SSL;
GRANT ALL PRIVILEGES ON raffapp.*
TO 'raffappuser'@'10.0.0.10';
SHOW GRANTS FOR 'raffappuser'@'10.0.0.10';
EXIT;
Store the generated password securely.
If UFW is active, allow only the application VM's private address:
sudo ufw allow from 10.0.0.10 to 10.0.0.5 port 3306 proto tcp
sudo ufw status numbered
From the application VM, require TLS:
mysql \
--host=10.0.0.5 \
--user=raffappuser \
--password \
--ssl-mode=REQUIRED \
raffapp
Verify encryption inside the session:
SHOW STATUS LIKE 'Ssl_cipher';
The cipher value is non-empty. Use VERIFY_CA or VERIFY_IDENTITY when your application has a CA and certificate model it can validate.
Verify: MySQL listens only on loopback plus the intended private address, UFW allows only the application VM, the MySQL account matches that source IP, and the remote session reports a TLS cipher.
Step 10 — Create a consistent logical backup
Create a protected root-owned backup directory:
sudo install -d -m 700 /var/backups/mysql
Create a custom backup filename:
STAMP="$(date -u +%Y%m%d-%H%M%S)"
BACKUP_FILE="/var/backups/mysql/raffapp-${STAMP}.sql.gz"
Dump the InnoDB application database using a consistent transaction snapshot. sudo tee writes the compressed stream into the root-owned backup directory without weakening directory permissions:
sudo mysqldump \
--single-transaction \
--quick \
--routines \
--triggers \
--events \
raffapp \
| gzip \
| sudo tee "$BACKUP_FILE" > /dev/null
Restrict the file:
sudo chmod 600 "$BACKUP_FILE"
Validate the archive and inspect its beginning:
sudo gzip -t "$BACKUP_FILE"
sudo zcat "$BACKUP_FILE" | head
--single-transaction provides a consistent snapshot for transactional tables such as InnoDB without a long global read lock. Check storage engines separately if you inherit older non-transactional tables.
Keep at least one production backup outside the database VM. Raff Object Storage can be used by S3-compatible backup tooling, while Data Protection provides an additional VM-level recovery layer.
Verify: the compressed logical dump exists with mode 600, gzip -t succeeds, and your production recovery design includes an off-server copy.
Step 11 — Restore into a disposable database
Never test a restore by overwriting production.
Create an isolated target:
sudo mysql -e \
"CREATE DATABASE raffapp_restore_test CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"
Select the newest tutorial backup:
LATEST_BACKUP="$(sudo ls -1t /var/backups/mysql/raffapp-*.sql.gz | head -n 1)"
echo "$LATEST_BACKUP"
Restore the dump directly into the disposable database:
sudo zcat "$LATEST_BACKUP" | sudo mysql raffapp_restore_test
Inspect the restored tables:
sudo mysql -e \
"SHOW TABLES FROM raffapp_restore_test;"
For a production recovery rehearsal, also validate representative row counts and application behavior. Use a dedicated recovery VM or isolated MySQL instance when integrations or scheduled jobs could react to restored data.
Remove only the disposable restore database after validation:
sudo mysql -e 'DROP DATABASE raffapp_restore_test;'
Verify: the dump imports into raffapp_restore_test, expected tables appear, production raffapp remains untouched, and the disposable target can be removed cleanly.
Step 12 — Apply MySQL updates safely
Check for available MySQL package updates:
apt list --upgradable 2>/dev/null | grep -E '^mysql|^libmysql' || true
Before a MySQL package update:
- create a fresh database backup;
- confirm an off-server copy exists;
- review the Ubuntu changelog and upstream release notes;
- verify disk headroom;
- schedule a maintenance window when the workload requires it.
Apply Ubuntu updates:
sudo apt update
sudo apt upgrade
Verify MySQL after package changes:
systemctl is-active mysql
sudo mysql -NBe 'SELECT VERSION();'
sudo mysqladmin ping
sudo journalctl -u mysql --no-pager -n 100
Treat a move from Ubuntu's MySQL 8.0 packages to Oracle's separate APT repository as a migration, not a routine patch.
Verify: MySQL is active on the expected package branch, local administration works, and the application authentication and CRUD path still succeeds after updates.
Step 13 — Review configuration and recovery readiness
Validate the active configuration:
sudo mysqld --validate-config
Inspect key runtime values:
sudo mysql -NBe \
"SELECT @@version, @@bind_address, @@port, @@require_secure_transport;"
List application storage engines:
sudo mysql -NBe \
"SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA='raffapp';"
Review recent logs:
sudo journalctl -u mysql --no-pager -n 100
sudo tail -n 100 /var/log/mysql/error.log
Check disk growth:
df -h /
sudo du -sh /var/lib/mysql /var/backups/mysql
Do not copy generic innodb_buffer_pool_size, max_connections, or durability values into production. Measure the workload first and change one documented setting at a time.
Verify: configuration validation succeeds, application tables use the intended storage engine, logs show no unresolved startup or storage errors, and backup growth does not threaten live data capacity.
Step 14 — Verify MySQL end to end
Run the final service, account, network, and backup checks:
echo 'Service:'
systemctl is-active mysql
systemctl is-enabled mysql
echo 'Version:'
sudo mysql -NBe 'SELECT VERSION();'
echo 'Root account:'
sudo mysql -NBe \
"SELECT user, host, plugin FROM mysql.user WHERE user='root';"
echo 'Application accounts:'
sudo mysql -NBe \
"SELECT user, host, plugin, ssl_type FROM mysql.user WHERE user='raffappuser';"
echo 'Listener:'
sudo ss -lntp | grep ':3306' || true
echo 'Firewall:'
sudo ufw status numbered
echo 'Recent backups:'
sudo ls -lh /var/backups/mysql/raffapp-*.sql.gz | tail
Confirm all of these are true:
- Ubuntu 24.04 manages the installation through its MySQL 8.0 packages;
- MySQL is active and enabled;
- root administration is local-only;
- anonymous users and the test database are absent;
- the application uses a separate
caching_sha2_password account;
- application privileges are scoped to the application database;
- TCP 3306 is not publicly exposed;
- optional private access is restricted to an exact source IP and uses TLS;
- a logical backup exists and has been restored into an isolated database;
- package updates are backed up and verified.
Verify: the installation passes every check above, including successful application CRUD from Step 7 and the isolated restore from Step 11.
Cleanup (optional)
Use this section only when the database, users, firewall rules, and backup files were created solely for this tutorial.
Warning
The commands below can permanently delete the tutorial database, database users, and backups. Keep anything you need before continuing.
Drop the tutorial database and local application user:
sudo mysql <<'SQL'
DROP DATABASE IF EXISTS raffapp;
DROP USER IF EXISTS 'raffappuser'@'localhost';
DROP USER IF EXISTS 'raffappuser'@'10.0.0.10';
SQL
If you completed the private-access step, restore the saved listener configuration:
if [ -f /etc/mysql/mysql.conf.d/mysqld.cnf.before-private-access ]; then
sudo cp /etc/mysql/mysql.conf.d/mysqld.cnf.before-private-access \
/etc/mysql/mysql.conf.d/mysqld.cnf
sudo mysqld --validate-config
sudo systemctl restart mysql
fi
Delete only the UFW rules you added for this tutorial:
sudo ufw delete allow from 10.0.0.10 to 10.0.0.5 port 3306 proto tcp
sudo ufw delete deny 3306/tcp
Remove tutorial backups if they are disposable:
sudo rm -f /var/backups/mysql/raffapp-*.sql.gz
Verify the tutorial state is gone:
sudo mysql -NBe \
"SELECT schema_name FROM information_schema.schemata WHERE schema_name='raffapp';"
sudo mysql -NBe \
"SELECT user, host FROM mysql.user WHERE user='raffappuser';"
sudo ufw status numbered
The database and application-user queries return no rows.
Troubleshooting
mysql.service is not found
This usually means the MySQL server package is not installed or the installation did not complete. Inspect package state:
dpkg -l | grep -E '^ii\s+mysql-server'
apt-cache policy mysql-server mysql-server-8.0
Install the Ubuntu package and verify the unit:
sudo apt update
sudo apt install -y mysql-server
systemctl status mysql --no-pager
Verify: systemctl status mysql finds the unit and reports the installed service state.
mysql_secure_installation: command not found
Ubuntu's MySQL 8.0 server-core package contains /usr/bin/mysql_secure_installation. Verify the binary and package:
command -v mysql_secure_installation || true
dpkg -S /usr/bin/mysql_secure_installation 2>/dev/null || true
dpkg -l | grep -E '^ii\s+mysql-server-core-8.0'
If the package is installed but the binary is missing, reinstall the matching Ubuntu package:
sudo apt install --reinstall mysql-server-core-8.0
command -v mysql_secure_installation
Verify: the command resolves to /usr/bin/mysql_secure_installation.
Access denied for user 'root'@'localhost'
Confirm service health, package source, and the server log before attempting any root-account reset:
systemctl is-active mysql
apt-cache policy mysql-server-8.0
sudo tail -n 100 /var/log/mysql/error.log
If sudo mysql previously worked and stopped working after account changes, inspect what changed rather than applying a password-reset procedure written for another distribution or package source. MySQL can use socket-based authentication for tightly restricted local administrative accounts.
MySQL does not start after editing mysqld.cnf
Validate the configuration and inspect logs:
sudo mysqld --validate-config
sudo journalctl -u mysql --no-pager -n 100
sudo tail -n 100 /var/log/mysql/error.log
Restore the pre-change configuration if validation fails.
The app cannot authenticate with caching_sha2_password
Upgrade the application's MySQL connector or runtime. MySQL 8.0 uses caching_sha2_password by default. Do not weaken the account to a legacy authentication plugin as the first fix.
A private remote connection times out
Check the listener, firewall, and account host together:
sudo mysql -NBe 'SELECT @@bind_address, @@port;'
sudo ss -lntp | grep ':3306'
sudo ufw status numbered
sudo mysql -NBe \
"SELECT user, host, plugin, ssl_type FROM mysql.user WHERE user='raffappuser';"
The application VM's source IP must match the listener, UFW rule, and MySQL account host.
A private connection is not encrypted
Connect with --ssl-mode=REQUIRED or stronger, then run:
SHOW STATUS LIKE 'Ssl_cipher';
A non-empty value confirms TLS for the session.
mysqldump or restore fails
Confirm the database exists, inspect storage engines, and verify free disk space:
sudo mysql -e 'SHOW DATABASES;'
sudo mysql -e \
"SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA='raffapp';"
df -h /
For larger databases or tighter recovery objectives, logical dumps may need to be combined with additional database-aware and infrastructure recovery controls.
FAQ
How do I install MySQL on Ubuntu 24.04?
Run sudo apt update and sudo apt install -y mysql-server, then verify the service with systemctl is-active mysql and the server version with sudo mysql -NBe 'SELECT VERSION();'.
What MySQL version does Ubuntu 24.04 install?
Ubuntu 24.04 currently tracks MySQL 8.0. The current Noble package is MySQL 8.0.46 as of September 17, 2026.
Why does sudo mysql work without a MySQL root password?
MySQL supports socket peer-credential authentication for local administrative accounts. Verify the actual root plugin with SELECT user, host, plugin FROM mysql.user WHERE user='root';.
Should I run mysql_secure_installation?
Yes for this tutorial. Verify the resulting account and database state afterward instead of relying only on the interactive prompts.
Should port 3306 be public?
No. Keep MySQL local when the app shares the VM. For a separate app server, use private networking, an exact source-IP rule, and TLS.
Which authentication plugin should app users use?
Use caching_sha2_password with a current connector. It is the default authentication plugin in MySQL 8.0.
Is mysqldump enough for production recovery?
It can be one recovery layer. Production plans also need off-server copies, retention, restore testing, monitoring, and recovery objectives that match the workload.
Conclusion
You now have MySQL 8.0 running on a Raff Ubuntu 24.04 VM with local-only administration, a dedicated application account, verified CRUD, private-by-default networking, a protected logical backup, and an isolated restore test.
For production planning, continue with MySQL Hosting: Managed vs Self-Hosted, Set Up a LEMP Stack on Ubuntu 24.04, and Set Up UFW Firewall on Ubuntu 24.04. If you prefer to avoid host-level database operations, compare Raff Managed MySQL.
Sources