Getting MySQL Running

Before any SQL you need a server you trust: the right series, from a source that keeps patching it, locked down, and configured in a file you can find again. This section takes MySQL 9.7 LTS from download to a client session on Ubuntu, Windows, macOS and Docker. Packages and systemd are in Chapter 1.

3.1.1 What MySQL Is and Where It Sits in the LAMP Stack

MySQL is a relational database server: a program, mysqld, that owns a data directory and answers SQL from clients. Unlike SQLite it is not a library your code loads. PHP, the mysql command line and Workbench are all clients that connect, authenticate as an account, send statements and read result sets, so one server can serve a hundred PHP-FPM workers.Figure 3.1: Where MySQL sits in a LAMP request, and the two ways a client reaches itInside mysqld each connection gets a thread. The SQL layer parses a statement, the optimizer chooses a plan (Section 3.12), and a storage engine reads and writes rows: InnoDB, with transactions, row locks and crash recovery (Section 3.14). Accounts and metadata live in the mysql system schema in the same data directory. Under the hood: To MySQL clients, localhost means "use the Unix socket file", which skips TCP and tells the server which operating-system user is connecting; 127.0.0.1 forces TCP. The two can meet different authentication rules, as Section 3.1.3 shows. MySQL AB released MySQL on 23 May 1995, named after co-founder Michael "Monty" Widenius's daughter My. Sun bought the company for $1 billion in 2008, and Oracle's purchase of Sun (January
  • prompted Widenius to fork MariaDB. The Community Edition is GPLv2; the commercial

  • Enterprise Edition adds plugins such as audit and thread pool. Percona Server rebuilds each MySQL LTS, while MariaDB has diverged (its JSON is an alias for LONGTEXT); see Section 3.18.5.

    3.1.2 The Release Model: LTS, Innovation, and Calendar Versioning

    MySQL 8.0 changed features and defaults across eight years of point releases. Since 2023 releases follow two tracks instead:
  • LTS (long-term support) series get only necessary fixes: "There are no removals within an LTS release," and features change only in its first release. Each gets five years of premier and three of extended support under Oracle's Lifetime Support Policy.

  • Innovation releases carry new features and behavior changes, each supported only until the next one, roughly a quarter later.

  • After the Innovation releases 8.1 to 8.3 came 8.4, the first LTS; then Innovation 9.0 to 9.6; then 9.7, the second LTS and the last sequential number. Releases now use calendar versions, YY.M: MySQL 26.7 is the July 2026 Innovation release. Within an LTS only the last number moves. Series Track First release Status in September 2026 8.0 Old GA model 2018 Sustaining Support since 21 Apr 2026 8.4 LTS 30 Apr 2024 Premier to 2029, extended to 2032 9.0 to 9.6 Innovation Jul 2024 to Jan 2026 Ended 9.7 LTS 21 Apr 2026 Newest 9.7.2; premier to 2031, extended to 2034 26.7 Innovation 28 Jul 2026 Newest 26.7.1; until the next Innovation Table 3.1: MySQL server series and their support status "Sustaining Support" means no new fixes, security included; Oracle directs 8.0 users to 8.4 or 9.7. The 9.7 patches so far are 9.7.0 (21 April), 9.7.1 (16 June) and 9.7.2 (28 July 2026); the 9.7.3 of 18 August is a security patch "for the MySQL Server docker image, only", and both Oracle's Ubuntu repository and Docker Hub's mysql:9.7 carry 9.7.2.Upgrades follow compatibility lineage, "a continuous direct-upgrade sequence bounded by LTS releases": you may go from an LTS to a later Innovation release or to the next LTS, but never skip an LTS. An 8.0 server goes to 8.4, then 9.7; MySQL Shell's upgrade checker (Section 3.1.9) tests each step first.Choose 9.7 LTS for anything new, keep 8.4 LTS where a fleet or managed service still runs it, and try Innovation releases only where you can rebuild every quarter. Oracle's download page preselects the newest Innovation release: pick "9.7.2 LTS" from its list.

    3.1.3 Installing MySQL on Ubuntu

    On Ubuntu 26.04, sudo apt install mysql-server gives you Ubuntu's 8.4.11 LTS, patched through unattended-upgrades. For 9.7 LTS you add Oracle's APT repository with the small mysql-apt-config package. Its debconf question, which component to enable, opens a text dialog that hangs a scripted install, so answer it in advance:

    curl -LO https://dev.mysql.com/get/mysql-apt-config_0.8.40-1_all.deb

    echo "mysql-apt-config mysql-apt-config/select-server select mysql-9.7-lts" \

    | sudo debconf-set-selections

    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ./mysql-apt-config_0.8.40-1_all.deb

    sudo apt-get update

    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server

    Listing 3.1: Installing MySQL 9.7 LTS from Oracle's APT repository, unattended Warning: On Ubuntu 26.04 sudo is sudo-rs, which answers sudo -E with preserving the entire environment is not supported, '-E' is ignored: an exported DEBIAN_FRONTEND is lost and the install blocks on the dialog. Put the variable after sudo. Without the preseeded answer (or MYSQL_SERVER_VERSION) you silently get mysql-8.4-lts. That is how this book's server was installed. Oracle's 9.7.2 (***, installed) outranks Ubuntu's 8.4 builds because all sit at priority 500 and APT prefers the higher version (Section 1.4.3):

    grep '^deb ' /etc/apt/sources.list.d/mysql.list | cut -d' ' -f3-

    apt-cache policy mysql-server | grep -E '^ {1,5}(\*\*\* )?[0-9]' # versions on offer

    systemctl status mysql --no-pager | grep -E 'Loaded:|Active:|Status:'

    Listing 3.2: Confirming what was installed and where it came from

    http://repo.mysql.com/apt/ubuntu/ resolute mysql-apt-config

    http://repo.mysql.com/apt/ubuntu/ resolute mysql-9.7-lts

    http://repo.mysql.com/apt/ubuntu/ resolute mysql-tools

    *** 9.7.2-1ubuntu26.04 500

    8.4.11-0ubuntu0.26.04.1 500

    8.4.8-0ubuntu1 500

    Loaded: loaded (/usr/lib/systemd/system/mysql.service; enabled; preset: enabled)

    Active: active (running) since Wed 2026-09-23 14:47:23 +08; 33min ago

    Status: "Server is operational"

    Output of Listing 3.2 The service starts at boot, and Status: is reported by mysqld itself once crash recovery is done. Root uses the auth_socket plugin:

    sudo mysql -N -e "SELECT CURRENT_USER()"

    mysql -u root -e "SELECT 1"

    sudo mysql -h 127.0.0.1 -e "SELECT 1"

    Listing 3.3: root authenticates by operating-system user, over the socket only

    root@localhost

    ERROR 1698 (28000): Access denied for user 'root'@'localhost'

    ERROR 1698 (28000): Access denied for user 'root'@'localhost'

    Output of Listing 3.3 auth_socket reads the client's Unix user from the socket (SO_PEERCRED) and admits it only if it matches the account name. sudo mysql is Unix root on the socket; your own login is not; and -h 127.0.0.1 fails even under sudo, because TCP carries no peer credentials. Nobody can guess a password root does not have: keep it so, and give applications their own accounts (Section 3.16.9). unattended-upgrades ignores Oracle's repository (Section 1.4.4), so run apt upgrade after each quarterly patch.

    3.1.4 Installing MySQL on Windows

    The "MySQL Installer" of older tutorials belongs to the 8.0 era. For 9.7 Oracle ships an MSI (mysql-9.7.2-winx64.msi, about 175 MB) or a ZIP, both containing MySQL Configurator, which must run before the server will start. It needs 64-bit Windows (Oracle lists Windows 11 and Server 2022) and the Visual C++ 2019 redistributable. Configurator asks for a server type that sizes memory, ports 3306 and 33060, the root password, optional sakila and world sample databases, and a Windows service. Its command line, from Oracle's manual (not run here, on Linux), skips the clicks:

    msiexec /i mysql-9.7.2-winx64.msi /qn /lv install.log

    cd "C:\Program Files\MySQL\MySQL Server 9.7\bin"

    .\mysql_configurator.exe --console --action=configure --password=S3cure-Root! `

    --port=3306 --server-type=Developer --install-sample-databases=None

    Listing 3.4: Unattended install and configuration on Windows (elevated prompt) Data, logs and my.ini go to the hidden C:\ProgramData\MySQL\MySQL Server 9.7; the service is named after the series (MySQL97); add bin to PATH to run mysql anywhere. Warning: winget install Oracle.MySQL tops out at 8.4.9 in September 2026, two patches behind even 8.4. Use the MSI, or run the Linux build inside WSL2 as in Section 3.1.3.

    3.1.5 Installing on macOS and with XAMPP

    Oracle's macOS build is a DMG with a native package (mysql-9.7.2-macos15-arm64.dmg; 9.7 is supported on macOS 14 and 15 on Apple silicon). It asks for a root password, installs into a versioned folder behind a /usr/local/mysql symlink, and registers the launch daemon com.oracle.oss.mysql.mysqld.plist, whose arguments override my.cnf.Homebrew's mysql formula is the 26.7.0 Innovation release, so ask for mysql@9.7 (9.7.2). It is keg-only, not linked into Homebrew's bin. These commands follow the formula's notes and were not run on this Linux machine:

    brew install mysql@9.7

    echo 'export PATH="$(brew --prefix mysql@9.7)/bin:$PATH"' >> ~/.zshrc

    brew services start mysql@9.7 # launchd agent, restarts at login

    mysql_secure_installation # root starts with no password

    Listing 3.5: MySQL 9.7 LTS with Homebrew on macOS It listens on localhost only, and root has no password until you secure it. Coming from a formula older than 8.4, run 8.4 once first (Section 3.1.2).XAMPP, where many PHP tutorials still start, is the wrong choice here. Its newest release, 8.2.12 of November 2023, ships MariaDB 10.4.32, not MySQL, and MariaDB 10.4 reached end of life on 18 June 2024 (Section 1.11.6). For one Windows installer with MySQL 9.7.2 LTS, use WampServer (Section 1.11.5), or run the server in Docker (Section 3.1.6).

    3.1.6 Running MySQL in Docker

    A container is a disposable server of any version. The official mysql:9.7 image (Oracle Linux 9, amd64 and arm64) serves every example in this chapter that changes server settings. The system server holds port 3306, so this one publishes 3301, on loopback only:

    docker run -d --name mysql97 -e MYSQL_ROOT_PASSWORD=secret \

    -p 127.0.0.1:3301:3306 -v mysql97-data:/var/lib/mysql mysql:9.7

    docker logs mysql97 2>&1 \

    | grep -E '\[Entrypoint\]: (Init|Starting temp|Stopping temp|MySQL init)' | cut -c12-

    docker ps --filter name=mysql97 --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

    Listing 3.6: A throwaway MySQL 9.7 server with its data in a named volume

    2d30291c4677ef62e9363371c8c5ced606b17a8f7675f8d4218d7d57069db6ac

    07:07:32+00:00 [Note] [Entrypoint]: Initializing database files

    07:07:42+00:00 [Note] [Entrypoint]: Starting temporary server

    07:07:47+00:00 [Note] [Entrypoint]: Stopping temporary server

    07:07:48+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.

    NAMES STATUS PORTS

    mysql97 Up 11 minutes 33060/tcp, 127.0.0.1:3301->3306/tcp

    Output of Listing 3.6 On the first start, with an empty data directory, the entrypoint initializes the system tables, starts a temporary server on port 0 (no networking), creates root (for localhost and '%') plus any MYSQL_DATABASE, MYSQL_USER and MYSQL_PASSWORD you passed, runs the .sh and .sql files in /docker-entrypoint-initdb.d, and restarts for real. Later starts find data in the volume and skip all of it, so changing those variables afterwards does nothing.Connect with docker exec -it mysql97 mysql -uroot -p, or through 127.0.0.1:3301. The volume outlives the container: after docker rm -f mysql97, a new container with the same -v finds every database. Option files go in /etc/mysql/conf.d (Section 3.1.8); Section 1.11.7 adds PHP. Warning: docker inspect printed MYSQL_ROOT_PASSWORD=secret here for anyone allowed to run it. Outside a laptop, set MYSQL_ROOT_PASSWORD_FILE to a Docker secret's path instead.

    3.1.7 Securing a Fresh Installation

    mysql_secure_installation makes four fixes: a root password, no remote root, no anonymous accounts, no test database. First it offers the validate_password component, which checks every later password. The mysql:9.7 image lacks the tool, but it is an ordinary client, so the Ubuntu host's copy secures the container of Section 3.1.6 (answers follow each prompt): mysql_secure_installation -h 127.0.0.1 -P 3301 -u root Listing 3.7: Hardening the container's server from the host

    Enter password for user root:

    ...

    Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 2

    ...

    Estimated strength of the password: 25

    Change the password for root ? ((Press y|Y for Yes, any other key for No) : n

    ...

    Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y

    Success.

    ...

    All done!

    Output of Listing 3.7 STRONG demands eight characters, digits, mixed case, a symbol and, if configured, a dictionary check; 25 out of 100 is its verdict on root's secret. The changes apply at once:

    mysql -h 127.0.0.1 -P 3301 -uroot -psecret -e "SELECT 1"

    docker exec mysql97 mysql -uroot -psecret \

    -e "CREATE USER 'report'@'localhost' IDENTIFIED BY 'bookshop2026'"

    Listing 3.8: Root from the network, and a weak password, are now refused

    mysql: [Warning] Using a password on the command line interface can be insecure.

    ERROR 1130 (HY000): Host '172.17.0.1' is not allowed to connect to this MySQL server

    mysql: [Warning] Using a password on the command line interface can be insecure.

    ERROR 1819 (HY000) at line 1: Your password does not satisfy the current policy requirements

    Output of Listing 3.8 The host arrives from the Docker bridge address, which only the deleted root@'%' matched; docker exec still works through the container's socket. VALIDATE_PASSWORD_STRENGTH() scores a candidate first: bookshop2026 got 50 here, Bk-Shop#2026 100. On Ubuntu, root's auth_socket leaves no password to guess, but the other fixes still apply (Section 3.16.2).

    3.1.8 The Option File and Server Variables

    MySQL programs read INI-style option files at startup: [mysqld] for the server, [client] and [mysql] for clients. On Ubuntu that means /etc/my.cnf, /etc/mysql/my.cnf and ~/.my.cnf, later values winning (Section 1.2.2). /etc/mysql/my.cnf links to mysql.cnf, which includes conf.d/ and then mysql.conf.d/, home of the package's mysqld.cnf. Leave packaged files alone and add your own that sorts last:

    [mysqld]

    max_connections = 200

    innodb_buffer_pool_size = 512M

    slow_query_log = ON

    long_query_time = 0.5

    Listing 3.9: mysql.conf.d/zz-app.cnf: local overrides that survive package upgrades Check the merge with my_print_defaults mysqld, then restart. (On Windows the file is my.ini in C:\ProgramData\MySQL\MySQL Server 9.7.) Copied into the Docker server's conf.d/ from a Windows folder, this file arrived with mode 777 and was rejected: World-writable config file '/etc/mysql/conf.d/zz-app.cnf' is ignored. Keep option files at mode 644.Most settings are also system variables you can change at runtime. In the Docker server:

    SET GLOBAL max_connections = 300; -- every new connection, until restart

    SET PERSIST long_query_time = 1; -- now, and after every restart

    SET PERSIST_ONLY back_log = 300; -- read-only at runtime: next start only

    SET GLOBAL back_log = 300;

    Listing 3.10: Global and persisted changes ERROR 1238 (HY000) at line 4: Variable 'back_log' is a read only variable Output of Listing 3.10 PERSIST writes JSON to mysqld-auto.cnf in the data directory, read after all other option files. After a restart, the Performance Schema names each value's source:

    SELECT VARIABLE_NAME, VARIABLE_SOURCE, VARIABLE_PATH

    FROM performance_schema.variables_info

    WHERE VARIABLE_NAME IN ('max_connections', 'long_query_time', 'back_log', 'sort_buffer_size');

    Listing 3.11: Where did each setting come from?

    +------------------+-----------------+--------------------------------+

    | VARIABLE_NAME | VARIABLE_SOURCE | VARIABLE_PATH |

    +------------------+-----------------+--------------------------------+

    | back_log | PERSISTED | /var/lib/mysql/mysqld-auto.cnf |

    | long_query_time | PERSISTED | /var/lib/mysql/mysqld-auto.cnf |

    | max_connections | GLOBAL | /etc/mysql/conf.d/zz-app.cnf |

    | sort_buffer_size | COMPILED | |

    +------------------+-----------------+--------------------------------+

    Output of Listing 3.11 SET GLOBAL did not survive (the file's 200 is back); both persisted values did. PERSIST suits servers whose files you cannot edit, but hides settings from anyone reading my.cnf. Undo with RESET PERSIST long_query_time, never by editing the file. Section 3.13 tunes the variables that matter.

    3.1.9 The mysql Client and MySQL Shell

    Keep passwords off the mysql command line, where ps and shell history see them: mysql_config_editor set --login-path=dev97 --host=127.0.0.1 --port=3301 --user=webdev --password prompts once and stores them, obfuscated, in ~/.mylogin.cnf. Here webdev is an account on the Docker server, which holds the shop example database:

    mysql --login-path=dev97 shop -N -B -e "SELECT sku, price FROM products ORDER BY id LIMIT 3" \

    | tr '\t' ','

    mysql --login-path=dev97 shop -e 'SELECT id, sku FROM products ORDER BY id LIMIT 1\G'

    mysql --login-path=dev97 shop --commands \

    -e 'SELECT id, sku FROM products ORDER BY id LIMIT 1\G'

    Listing 3.12: Login paths, script-friendly output, and batch-mode limits

    BK-PHP-01,39.90

    BK-SQL-01,44.50

    BK-SQL-02,29.00

    ERROR at line 1: Unknown command '\G'.

    *************************** 1. row ***************************

    id: 1

    sku: BK-PHP-01

    Output of Listing 3.12 -N -B prints bare tab-separated rows for scripts, -t draws tables and \G one column per line. The second command catches people out: in batch mode (-e, a pipe, a script) the 9.7 client refuses its own commands such as \G, status and source unless you pass --commands. Interactively they work, and \s reports the connection type, TLS cipher and character sets.MySQL Shell (mysqlsh) adds JavaScript and Python modes (\js, \py) with a session object for scripting, the X DevAPI on port 33060, and utilities such as util.checkForServerUpgrade() and util.dumpInstance() (Section 3.17.5). Since 26.7 one Shell version serves every server from 8.4 up. For Ubuntu 26.04 Oracle publishes it only in the Innovation component, so this book installed the downloaded mysql-shell_26.7.1-1ubuntu26.04_amd64.deb with sudo apt-get install ./<file>, leaving the server on 9.7 LTS. Here $PW holds the account's password:

    echo "$PW" | mysqlsh --sql --uri webdev@127.0.0.1:3301/shop --passwords-from-stdin \

    --result-format=json/array -e "SELECT sku, price FROM products ORDER BY price DESC LIMIT 2"

    Listing 3.13: MySQL Shell as a script-friendly client that speaks JSON

    [

    {"sku":"BK-LAR-01","price":49},

    {"sku":"BK-SQL-01","price":44.5}

    ]

    Output of Listing 3.13

    3.1.10 MySQL Workbench and Other GUI Clients

    MySQL Workbench 26.7.0 (July 2026) is a new program under an old name: an Electron application built on MySQL Shell that replaces Workbench 8.0 (last release 8.0.47) and adds SQL notebooks mixing queries, results and notes. It supports MySQL 8.4 and later, and it is young: a September 2026 forum report has it failing to connect to MariaDB where 8.0.47 worked. Client Version (Sep 2026) License Runs on Best at MySQL Workbench 26.7.0 GPLv2 Win, macOS, Linux Oracle's own tooling, notebooks DBeaver Community 26.2.1 Apache-2.0 Win, macOS, Linux Many databases in one tool HeidiSQL 12.21 GPL-2.0 Win, Linux, macOS Fast, light, Windows-first Beekeeper Studio 6.1.2 GPLv3 core Win, macOS, Linux Clean UI; paid extras phpMyAdmin 5.2.3 GPL-2.0 Web (PHP) Shared hosting, browser access Adminer 6.1.0 Apache-2.0 or GPL-2.0 Web (one PHP file) Quick access, easy to delete Table 3.2: GUI clients for MySQL Reach a remote server through an SSH tunnel (Section 1.7.5), not an open port 3306. The web tools are PHP applications on your server, and so attack surface at predictable URLs such as /phpmyadmin: put one behind authentication and an IP rule (Section 2.8), or upload Adminer's single file for a task and delete it afterwards. Commercial clients (DataGrip, TablePlus, Navicat) add polish for a license fee; this book needs none.