How to install MySQL on Ubuntu and create a database (step-by-step)
Install MySQL on Ubuntu and create a database from scratch: secure setup, dedicated user, permissions and remote connection test.
MySQL is still the most widely used relational database in web applications — Laravel, Django, Rails and Node.js all have first-class support. But the default Ubuntu install leaves loose ends: a root user without a strong password, no application database, and a network configuration that either blocks legitimate connections or exposes port 3306 to the world.
This guide covers the full path on an Ubuntu 24.04 LTS VPS: installing the official packages, basic hardening with mysql_secure_installation, creating a dedicated database for your application, creating a user with the minimum required permissions, and validating the connection. Target persona: a developer who needs a working local MySQL to run their application in staging or small production.
Estimated execution time: 15 to 20 minutes, counting service restarts and the application connection test.
Prerequisites
Before you start, confirm that your VPS meets the minimum below. If you do not have the server set up yet, the tutorial assumes working SSH access as a user with sudo.
Ubuntu 24.04 LTS (also works on 22.04), SSH access with a sudo user, at least 1 GB of RAM (MySQL 8 uses ~400 MB idle), and ~500 MB of free disk for the packages and the initial data directory at /var/lib/mysql.
MySQL 8.0.x 3306 /var/lib/mysql /etc/mysql/mysql.conf.d/mysqld.cnf If you still need to decide between running everything on one VPS or splitting the database onto a dedicated server, the rule of thumb is: up to ~5k active users/day, MySQL and the application on the same VPS saves network latency and simplifies deployment. Above that, split into dedicated instances.
Installing MySQL Server
Ubuntu 24.04 already ships MySQL 8.0 in the official repositories — no external PPA needed. This section covers installing the packages, first service start, and verifying that the daemon came up correctly.
Update the APT package index:
sudo apt updateThis command syncs the local list with the configured repositories. Skipping this step can result in installing an old version or a “package not found” error even when the package is available.
Install the mysql-server package:
sudo apt install -y mysql-serverThe installer pulls the server, the CLI client (mysql), and dependencies like libmysqlclient21. On a VPS with a 100 Mbps link, the full install takes ~30 seconds.
Verify that the service is running:
sudo systemctl status mysqlThe output should show active (running) in green. If you see inactive or failed, run sudo journalctl -u mysql -n 50 to inspect the error — in 90% of cases it is a lack of memory (killing unnecessary processes or increasing swap fixes it).
Enable auto-start on boot:
sudo systemctl enable mysqlWithout this, MySQL does not come up after a VPS reboot — a common bug that only surfaces on the first maintenance reboot, usually months later.
Initial hardening with mysql_secure_installation
Fresh MySQL ships with several insecure defaults to ease first use: a public test database, anonymous users, root reachable remotely. The mysql_secure_installation script fixes all of that in one go.
Run the interactive script:
sudo mysql_secure_installationIt will ask you sequentially:
- Validate password component: answer
Yand choose level2(STRONG) — enforces 8+ characters, mixed case, numbers and symbols. - Set root password: pick a strong password and save it in your password manager (1Password, Bitwarden, KeePassXC). You will need it for the next steps.
- Remove anonymous users:
Y. - Disallow root login remotely:
Y— root should only be reachable through the local socket. - Remove test database:
Y. - Reload privilege tables:
Y.
On MySQL 8 from Ubuntu, even after setting a password for root, sudo mysql still logs in without a prompt — that is because root uses auth_socket by default, which trusts the operating system UID. This is the expected and safe behavior for local administration.
Creating the database and the application user
Never use the root user for your application connections. The right approach is to create a dedicated database and a user with permissions restricted to that database only — so a compromise of the application does not become a compromise of the entire MySQL.
Connect to MySQL as root:
sudo mysqlYou will see the mysql> prompt. All commands below are SQL — they end with a semicolon.
Create the database with full UTF-8 encoding:
CREATE DATABASE my_app
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;Always use utf8mb4 (not plain utf8) — the legacy MySQL utf8 only stores 3 bytes per character and breaks on emojis and CJK characters. utf8mb4_unicode_ci provides correct case-insensitive ordering for English, Spanish, Portuguese and other Latin-script languages.
Create the application user with a strong password:
CREATE USER 'app_user'@'localhost'
IDENTIFIED BY 'StrongPasswordHere123!@#';Replace StrongPasswordHere123!@# with a randomly generated password — openssl rand -base64 24 in the terminal produces a suitable string. The @'localhost' restricts this user to local connections (via the Unix socket). If your application runs on another server, swap it for @'10.0.0.5' (a specific IP) or, as a last resort, @'%'.
Grant permissions only on the application database:
GRANT ALL PRIVILEGES ON my_app.*
TO 'app_user'@'localhost';
FLUSH PRIVILEGES;ALL PRIVILEGES ON my_app.* gives full control inside that database (CREATE, ALTER, INSERT, UPDATE, DELETE, SELECT, INDEX) but zero access to any other database. For deployments where the application does not create tables at runtime (migrations run separately), you can restrict further: GRANT SELECT, INSERT, UPDATE, DELETE ON my_app.* TO ....
Exit the MySQL prompt:
EXIT;In critical production, consider having two users: one for migrations (with ALL PRIVILEGES, used only at deploy time) and another for the application runtime (only SELECT/INSERT/UPDATE/DELETE). This limits the blast radius if a SQL injection slips through.
Verifying the setup
Before pointing the application at the database, validate everything from the CLI. If it fails here, it will fail there too — this saves debugging time.
Test the application user login:
mysql -u app_user -p my_appType the password when prompted. If you get in and see mysql> with no error, authentication and permissions are correct.
Create a test table and insert a row:
CREATE TABLE health_check (
id INT AUTO_INCREMENT PRIMARY KEY,
checked_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO health_check () VALUES ();
SELECT * FROM health_check;The output should show a single row with id=1 and the current date/time. If you get a permission error, go back to Step 09 and check the GRANT.
Clean up the test table:
DROP TABLE health_check;
EXIT;Configuring access from the application
With the database and user created, update your application’s configuration file. The examples below cover the most common formats.
For Laravel (.env):
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=my_app
DB_USERNAME=app_user
DB_PASSWORD=StrongPasswordHere123!@#
For Node.js with mysql2:
DATABASE_URL=mysql://app_user:StrongPasswordHere123!%40%[email protected]:3306/my_app
Note the URL-encoding in the password: ! becomes %21, @ becomes %40, # becomes %23. Forgetting this results in a confusing authentication error.
Add .env to .gitignore before the first commit. A database password leaked in a public repository is one of the most common causes of compromise — bots constantly scrape GitHub Search for strings like DB_PASSWORD=.
Troubleshooting
Error “Access denied for user ‘app_user’@‘localhost’”
The password you typed does not match the stored hash. Common causes:
- Special character not escaped in
.env(use double quotes around the password). - The user was created with
@'127.0.0.1'but the application connects aslocalhost(which uses the socket) — or the other way around. Create one user per host you need. - Password was changed via
SET PASSWORDinstead ofALTER USER, which in some versions keeps the old hash.
Quick fix:
ALTER USER 'app_user'@'localhost'
IDENTIFIED BY 'NewPassword';
FLUSH PRIVILEGES;
MySQL does not accept remote connections
By default MySQL 8 only listens on 127.0.0.1. To accept connections from other VMs on the same private network, edit /etc/mysql/mysql.conf.d/mysqld.cnf:
bind-address = 0.0.0.0
Restart the service and open the port in the firewall only for the authorized IPs:
sudo systemctl restart mysql
sudo ufw allow from 10.0.0.0/24 to any port 3306
Never open 3306 to 0.0.0.0/0 without TLS configured.
”Too many connections”
The default max_connections is 151. For applications with a large pool, bump it in mysqld.cnf:
max_connections = 500
Each connection uses ~12 MB of RAM — size it against your VPS before pushing this number up.
Next steps
With MySQL up and running, consider:
- Automated backups: schedule
mysqldumpin a daily cron and replicate the.sqlto external storage (S3, Backblaze B2). Without off-site backup, a corrupted disk wipes everything. - Monitoring: install
mysqltuner(sudo apt install mysqltuner) and run it after 24h of usage for tuning recommendations based on real workload. - TLS for remote connections: if you are going to expose 3306 to another VPS, set up certificates — MySQL 8 supports TLS natively.
- Read-only replication: for read-heavy applications, configure a secondary replica and route SELECTs to it.
- Dedicated VPS for the database: as your application grows, splitting MySQL onto a dedicated Hostini VPS with NVMe SSDs and snapshot backups reduces I/O contention between the app and the database.
Frequently asked questions
MySQL or MariaDB on Ubuntu 24.04?
For new projects, MySQL 8.0 ships more recent features (CTEs, window functions, optimized native JSON) and is what most ORMs document first. MariaDB is a compatible fork and performs slightly better in some read workloads, but if your application speaks MySQL, stick to MySQL to avoid subtle incompatibilities in new features.
Can I connect to MySQL from another server without exposing port 3306?
Yes, and this is the recommended approach. Use an SSH tunnel (ssh -L 3306:localhost:3306 user@server) or keep MySQL listening on 127.0.0.1 and run the application on the same VPS. Exposing 3306 publicly only makes sense with a strict per-IP firewall and mandatory TLS.
What is the difference between 'root'@'localhost' and 'root'@'%'?
The suffix after @ defines where that user can connect from. 'root'@'localhost' only accepts local socket connections; 'root'@'%' accepts from any IP. Always use localhost for root. For your app, create a user with a specific host ('app'@'10.0.0.5') or '%' only when the firewall already restricts IPs.
mysql_secure_installation removed the anonymous user, but I can still log in without a password. Why?
You are probably logged in as root on the operating system, and MySQL 8 uses auth_socket by default for the local root — it does not ask for a password because it trusts the OS user. To force a password, change it with ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'password'.
How do I back up the database after creating it?
Use mysqldump -u root -p database_name > backup.sql for a full SQL dump, or mysqldump --single-transaction for InnoDB databases without locking tables. To restore: mysql -u root -p database_name < backup.sql. In production, automate with a daily cron and keep at least 7 days of retention on separate storage.
Do I need to restart MySQL every time I change my.cnf?
Yes, changes to /etc/mysql/mysql.conf.d/mysqld.cnf require sudo systemctl restart mysql to take effect. Some variables are dynamic and can be tweaked at runtime with SET GLOBAL name = value, but they revert to the default on the next restart if they are not in the file.