MySQL runs a huge share of the world's applications. This guide gets it running three ways and covers the administration you'll actually use — plus the errors you'll actually hit.
Prerequisites
- A Linux machine (or VM) with sudo, plus Docker and a Kubernetes cluster for those sections.
- Basic SQL comfort (
SELECT,CREATE TABLE).
1. On Linux (the traditional way)
sudo apt update && sudo apt install -y mysql-server
sudo systemctl enable --now mysql
sudo mysql_secure_installation # set root auth, remove test db, disable remote root
Config lives in /etc/mysql/, data in /var/lib/mysql/, logs via journalctl -u mysql. Connect with
sudo mysql.
2. In Docker (fast and disposable)
docker run -d --name mysql \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=app \
-p 3306:3306 \
-v mysqldata:/var/lib/mysql \
mysql:8
The named volume is essential — without it, docker rm deletes your data. Ideal for local dev.
3. On Kubernetes
For learning, a StatefulSet + PVC works. For production, use an operator (Oracle's MySQL Operator or Percona's) which handles replication, backups, and failover:
apiVersion: mysql.oracle.com/v2
kind: InnoDBCluster
metadata: {name: app}
spec:
instances: 3 # primary + replicas with automatic failover
router: {instances: 1} # MySQL Router load-balances connections
secretName: mysql-root
Never hand-roll production HA when an operator exists — it encodes the hard-won failover logic.
Everyday administration
Users and privileges (least privilege!):
CREATE USER 'app'@'%' IDENTIFIED BY 'strong-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON app.* TO 'app'@'%';
FLUSH PRIVILEGES;
SHOW GRANTS FOR 'app'@'%';
Give each app its own user scoped to its own database — never share root.
Backups (logical):
mysqldump --single-transaction --routines --triggers app > app.sql # consistent, non-blocking on InnoDB
mysql app < app.sql # restore
For big databases use Percona XtraBackup (physical, faster, supports incrementals). Whatever you choose, test the restore — an untested backup isn't a backup.
Replication gives a read replica and failover target: enable binary logs on the primary, create a replication user, then on the replica:
CHANGE REPLICATION SOURCE TO SOURCE_HOST='primary', SOURCE_USER='repl',
SOURCE_PASSWORD='...', SOURCE_AUTO_POSITION=1;
START REPLICA;
SHOW REPLICA STATUS\G -- Replica_IO_Running and Replica_SQL_Running must both be Yes
Operators automate all of this.
Transactions, isolation & connections
Beyond installing MySQL, two topics separate people who run databases from people who just query them.
Transactions and isolation. InnoDB is transactional: wrap related writes in
START TRANSACTION ... COMMIT so they succeed or fail as a unit. MySQL's default isolation level is
REPEATABLE READ, which means a transaction sees a consistent snapshot for its whole life. Most bugs
here come from long-running transactions holding locks and blocking others — keep transactions
short, and never leave one open while waiting on user input or an external API. When two transactions
wait on each other you get a deadlock; InnoDB detects it and rolls one back with error 1213, so your
app should simply retry the losing transaction.
Connections and pooling. Each MySQL connection costs memory and a thread. Apps that open a
connection per request quickly hit Too many connections. The fix is almost never "raise
max_connections to 5000" — it's a connection pool (built into most frameworks) or ProxySQL in
front, which multiplexes many app connections onto a small, healthy set of database connections. Right-
size the pool to a few times your CPU count, not thousands.
Reading a slow query. The workflow is always the same: enable the slow query log, find the offender,
and run EXPLAIN on it:
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
-- type: ALL and rows: 2,000,000 → a full table scan; add the index:
CREATE INDEX idx_customer ON orders (customer_id);
-- re-run EXPLAIN → type: ref, rows: 12 → fixed.
type: ALL (full scan) and a huge rows estimate are your red flags; a well-chosen index turns a scan
into a lookup. Composite indexes should list the most selective, most-filtered column first.
High availability topologies
A single MySQL server is a single point of failure. Three common ways to fix that, in rising order of automation:
- Async replication (primary → one or more replicas). Simple, great for read scaling and a warm standby, but failover is manual and a crash can lose the last few unreplicated transactions. Add semi-synchronous replication so the primary waits for at least one replica to acknowledge, tightening the durability gap.
- InnoDB Cluster (Group Replication) — MySQL's built-in HA: a group of 3+ nodes with automatic failover and a consistent, quorum-based write model, fronted by MySQL Router which redirects apps to the current primary. This is the modern default for self-managed HA.
- Operator-managed on Kubernetes — the MySQL/Percona operators run InnoDB Cluster for you, handling
provisioning, failover, and backups declaratively (the
InnoDBClustermanifest earlier).
For a quick primer: pick semi-sync replication for a simple standby, InnoDB Cluster for automatic failover, and an operator when you're on Kubernetes. Whatever you choose, test failover — an untested HA setup is a hope, not a guarantee.
Zero-downtime schema migrations
ALTER TABLE on a large table can lock it for minutes and take your app down. On a busy production
database you use online schema change tools instead: pt-online-schema-change (Percona Toolkit) or
gh-ost (GitHub). They work by creating a shadow copy of the table, applying the change to it, copying
rows in the background while capturing ongoing writes, then swapping the tables atomically:
gh-ost --alter="ADD COLUMN country VARCHAR(2)" \
--database=app --table=orders --execute
The app keeps serving throughout. The discipline that pairs with this: make schema changes backward compatible and deploy in phases (add a nullable column → backfill → start writing → enforce), so a migration never requires a coordinated app-and-DB flip.
Security hardening
Beyond mysql_secure_installation, production MySQL should:
- Never expose 3306 to the internet. Keep it on a private network; reach it via the app tier or a bastion/VPN only.
- Require TLS for client connections so credentials and data aren't sent in clear text.
- Least-privilege every account — app users scoped to their database and the specific privileges they
need; no application using
root; separate accounts for backups (read + LOCK) and replication. - Rotate credentials and store them in a secrets manager, not in code or
my.cnfcommitted to git. - Keep it patched — MySQL security releases matter; operators and managed services handle this, self- hosted means you own it.
Monitoring: the metrics that matter
Watch these and you'll see trouble before users do: connections (approaching max_connections = an
imminent outage — pool!), replication lag (Seconds_Behind_Source), buffer pool hit ratio (low =
too little RAM for the working set), slow query count, InnoDB row lock waits/deadlocks, and
disk usage (binlogs and data). Export them with the Prometheus mysqld_exporter and alert on
connection saturation and replica lag especially.
Backups in depth: logical, physical & PITR
"We have backups" means nothing until you've restored one. Know the three levels:
- Logical (
mysqldump/mydumper) exports SQL statements. Portable across versions and machines, easy to inspect, but slow to restore on large databases. Great up to tens of GB. - Physical (Percona XtraBackup) copies the actual data files while the server runs. Far faster to back up and restore at scale, supports incrementals (only what changed), and is the right choice for large production databases.
- Point-in-time recovery (PITR) combines a base backup with the binary log so you can restore to
any moment — the thing that saves you after an accidental
DROP TABLEat 14:32. Keep binlogs, and you can replay up to 14:31.
The discipline that matters more than the tool: automate backups, store them off the server (another region/object storage), and test a restore on a schedule. A backup you've never restored is a hope, not a plan. Track your RPO (how much data you can lose) and RTO (how fast you must be back) and pick the method that meets them.
Storage engine, character sets & collation
Two settings quietly cause real bugs:
- Use InnoDB (the default). It's transactional, crash-safe, and row-locking. Avoid MyISAM for anything that matters — it has no transactions and table-level locks. If you inherit MyISAM tables, migrating them to InnoDB is usually a clear win.
- Use
utf8mb4, notutf8. MySQL's legacyutf8is a 3-byte encoding that cannot store emoji or some characters — a classic "why did this row break" bug.utf8mb4is real, full UTF-8. Set it at the database, table, and connection level, and pick a sensible collation (e.g.utf8mb4_0900_ai_cion MySQL 8) for correct sorting and comparison. Getting this right at creation time avoids a painful later migration.
Common production incidents
The MySQL pages you'll actually get, and the first thing to check:
Too many connections→ almost always a missing/leaking pool, not a real need for thousands of connections. Add a pool / ProxySQL; find the leak.- Replica lag climbing → a long single write or a hot table; check
Seconds_Behind_Sourceand consider parallel replication. - Disk full, server won't start → binlogs piled up; set
binlog_expire_logs_secondsand purge. - Sudden slowness after growth → a query that used to scan a small table now scans a large one; find it
in the slow log,
EXPLAIN, add the index. - Deadlocks (error 1213) → normal under concurrency; make the app retry the rolled-back transaction, and keep transactions short and consistently ordered.
Scaling reads and writes
When one MySQL server isn't enough, you scale in two directions — and it's worth knowing the ladder before you need it:
- Scale reads with replicas. Add read replicas and route read-heavy traffic (reports, search) to them while writes go to the primary. This handles the common case where reads vastly outnumber writes. Mind replication lag: don't read-your-own-write from a replica. A proxy like ProxySQL can split reads and writes automatically and route around a lagging or down replica.
- Scale writes with sharding — reluctantly. A single primary caps write throughput. When you truly exceed it, you shard: split data across multiple primaries by a shard key (user ID, tenant). Sharding multiplies write capacity but adds real complexity — cross-shard queries, rebalancing, and application awareness — so exhaust vertical scaling, replicas, caching, and query optimization first.
The pragmatic order is almost always: optimize queries and add indexes → add caching (Redis) → add read replicas → scale the instance up → and only then consider sharding. Most applications never need the last step, and reaching for it early buys complexity you'll regret.
Upgrades and version choice
Databases are long-lived, so plan versions deliberately. Prefer a recent LTS/GA major version (MySQL
8.x) for new projects — you get years of security patches and modern features (better JSON, window
functions, utf8mb4 defaults). For upgrades, the safe path is: read the release notes for breaking
changes, test on a copy of production data with your real queries, upgrade a replica first and
validate, then promote or roll the upgrade forward. Never upgrade a production primary in place without a
tested rollback. Managed services (RDS/Cloud SQL) make minor-version patching nearly automatic and give
guided major-version upgrades, but the "test on a replica first" discipline still applies. Skipping straight
across several major versions at once is where upgrades go wrong — step through them.
Tuning basics that matter
innodb_buffer_pool_size— the single most important setting; give InnoDB ~60–70% of RAM on a dedicated server.- Add indexes for columns in
WHERE/JOIN; find slow queries with the slow query log andEXPLAIN. - Keep transactions short to avoid lock contention.
Verify it worked
mysqladmin -u root -p status # uptime, threads, queries/sec
mysql -u app -p app -e "SELECT 1;" # the app user can connect and query
Troubleshooting
Access denied for user→ wrong host part of the account ('app'@'localhost'vs'app'@'%') or wrong password.SELECT user, host FROM mysql.user;.Too many connections→ raisemax_connections, but first check for a connection leak or add a pooler (ProxySQL). A leak is the usual cause.- Data gone after container restart (Docker) → no named volume. Always mount
/var/lib/mysql. - Replica lag growing → a long-running write or a single-threaded replica; check
SHOW REPLICA STATUSSeconds_Behind_Source, and consider parallel replication. - Disk full / server won't start → binary logs piled up. Set
binlog_expire_logs_secondsandPURGE BINARY LOGS. - Slow queries →
EXPLAINshows a full table scan → add the missing index.
Where to go next
Our free course installs MySQL all three ways, then walks through users, backups and restores, setting
up a replica, and reading EXPLAIN to fix a slow query — the core skills of a database administrator,
with hands-on labs.