# MySQL

MySQL 8 or MariaDB databases on the VPS with their own TLS-only account, grants on its database only and IP-based external access.

Source: https://ardumaker.com/en/docs/mysql/

- Category: Servers and infrastructure
- Status: Available
- Requirements: An active VPS with the `mysql` feature available; If the engine already exists: MySQL 8.x or MariaDB 10.4 or later; Being a member with access to the VPS to install; admin to reveal, open access or delete data
- MCP tools (Read only): `ver_catalogo_apps`, `ver_preflight_app`, `ver_app`, `ver_acceso_app`, `ver_operacion_app`, `listar_motores_db_vps`, `listar_bases_db_vps`
- MCP tools (Write or modify): `instalar_app`, `revelar_credencial_app`, `cambiar_lista_ip_app`, `reintentar_operacion_app`
- MCP tools (Destructive): `revocar_acceso_app`, `desinstalar_app`

## What can you do with it?

Every MySQL database is an app with its own account. The engine (MySQL or MariaDB) is one per VPS and shared by its databases; if it does not exist, MySQL is installed from the distribution packages.

- Create several `utf8mb4` databases, each with its account and grants.
- Reveal user, password and connection URI.
- Open the engine to the internet only for certain IPs, with TLS.
- From the VPS MySQL tab: databases, users, grants and tables.

## When should you use it?

- Applications that expect MySQL (PHP, Laravel, many third-party tools).
- A [WordPress](https://ardumaker.com/en/docs/wordpress) site uses this same engine, with its own database and account per site.

## How does it work in ArduMaker?

1. Engine: if the agent reports it is not installed, it is installed once (`mysql-server` and `mysql-client`); if it exists, it is never reinstalled nor upgraded.
2. Variant: `mysql --version` is read. Only MySQL 8.x or MariaDB 10.4+; anything else fails with `motor_no_soportado` without running SQL.
3. `app<id>_<hex>` credential with a random password, stored encrypted before touching the VPS.
4. It logs in as administrator through the socket, with the password in an ephemeral options file, never on the command line.
5. Account `'app<id>_<hex>'@'127.0.0.1'` with `REQUIRE SSL`, and `GRANT ALL PRIVILEGES` only on `<db>.*`, without `GRANT OPTION`, before creating the database.
6. `CREATE DATABASE <db> CHARACTER SET utf8mb4` only if it did not exist.
7. Verification: TLS required, no global privileges and grants on a single database, its own.

The account is `@'127.0.0.1'` on purpose: not `%` (it would open the account to any origin) nor `localhost` (socket, no TLS). When the server lacks `partial_revokes` (or is MariaDB), the `_` in the name is escaped in the GRANT, because there it is a wildcard.

`config.external_access` decides whether the database is reachable from the internet: `{"ips": [...]}` opens it only to those IPs; `{"local": true}` (or `false`) keeps it inside the VPS. If omitted, the dashboard uses the installer's IP; over MCP the agent's IP does not count, so without `ips` the database stays local instead of failing.

## What actually changes?

| What | Detail |
| --- | --- |
| Engine | MySQL or MariaDB listening on `127.0.0.1:3306` |
| Database | `utf8mb4`; the same name with different case counts as the same |
| Account | `'app<id>_<hex>'@'127.0.0.1'` REQUIRE SSL |
| TLS on MySQL 8 | `SET PERSIST` + `ALTER INSTANCE RELOAD TLS`, no restart |
| TLS on MariaDB | `90-ardumaker-tls.cnf` file; the first time requires restarting the engine |

> **INFO:** `mysql`, `information_schema`, `performance_schema` and `sys` are not accepted as names. `require_secure_transport` is not enabled globally: it would cut internal clients without TLS; TLS is required per account.

## Internet access and IP allowlist

By default the engine listens only inside the VPS (3306 on `127.0.0.1`). To use the database from outside, ArduMaker publishes an endpoint of the engine: `db-<VPS id>` inside the platform zone (direct DNS record, no proxy), on a reserved port between 61000 and 64999. On the host, a proxy carries that port to the VPS 3306 and the firewall lets in only the IPs on the list.

The certificate belongs to the `db-<id>` name and `mysqld` terminates it: the host proxy only passes TCP. The app account requires SSL wherever it comes from.

| List rule | Detail |
| --- | --- |
| Format | IPv4 or IPv6, with or without prefix; a single IP is /32 or /128. No names, loopback, link-local or multicast |
| Width | Minimum prefix /24 for IPv4 and /48 for IPv6 by default; `0.0.0.0/0` and `::/0` are rejected in the list |
| Size | Up to 20 entries; empty = closed to everyone |
| Host names | Up to 5 (`allowed_hosts`), with up to 8 public IPs each; re-resolved every 5 minutes, and a DNS failure keeps the last known value |
| Open to everyone | Only with `allow_any` and `confirm_open` together; the answer says so in `open_to_any` |

The list belongs to the engine, not to each database: changing it from any MySQL database changes access for every database of that engine on that VPS (the answer says so with `shared_with_engine` and `affected_apps`).

1. Missing firewall rules are added, one per CIDR (`ufw allow from <cidr> to any port <port> proto tcp`).
2. Then the extra ones are removed and their open connections are cut.
3. Only then is the listener created and verified: rules equal to the list, no rule open to anyone, listener present and a TLS handshake with the right name.

> **INFO:** Adding before removing means the intermediate state is always the union of two valid lists: there is never a moment open to everyone. If verification fails, the listener is removed.

> **PRECAUCION:** Another VPS on the same host does not get through the public IP even if it is on the list: the connection times out. The answer warns with `warnings` (`mismo_host`); between two such VPS an SSH tunnel is used (`configurar_tunel_entre_vps`).

## Uninstalling, persistence and backups

- Without `delete_data` (the default): the app's own external access is closed and it stops being listed as installed, but the database, its user and its data stay on the VPS. The next reconciliation shows it as discovered. Nothing is lost.
- With `delete_data: true` (confirmed twice, admins only): the database is dropped and, if Apps created the account, the account and its stored password too. Never a customer account nor `root`.
- The engine is never uninstalled from Apps: it stays installed even with no databases.
- If the VPS or the engine do not answer, `desinstalar_app` with `forzar=true` tries each step once, removes the app and lists in `not_cleaned` what was left on the server.
- Data lives on the VPS disk and travels in its weekly backup, which is taken while the VPS is running. For a consistent copy of a specific database it is better to also make your own dump from the [database manager](https://ardumaker.com/en/docs/gestor-de-bases-de-datos) (export) or with the engine's tools.

> **PELIGRO:** With `delete_data` the uninstall is irreversible.

## Diagram

**Connections to a MySQL database**

```text
App on the same VPS (`127.0.0.1:3306`)
  ↓
External client (`db-<id>:<port>` with TLS identity verification)
  ↓
Host firewall and proxy (listed IPs only → VPS 3306)
  ↓
mysqld (terminates TLS; the account requires SSL and sees only its database)
```

## Expected result

The URI (without password) appears once external access is ready; the full one is obtained with `revelar_credencial_app`.

```text
mysql://app<id>_<hex>@db-<vps>.<zona>:<puerto>/<base>?ssl-mode=VERIFY_IDENTITY
mysql://app<id>_<hex>@db-<vps>.<zona>:<puerto>/<base>?sslMode=verify-full   (MariaDB)
```

## Errors and troubleshooting

| Error | Cause | Retry? |
| --- | --- | --- |
| `motor_no_soportado` | Not MySQL 8.x nor MariaDB 10.4+; nothing is touched | No |
| `base_ya_existe` | A database with that name exists (case-insensitive) on which the account has no grants | No |
| `motor_detenido` / `base_no_creada` / `permisos_no_verificados` | Engine stopped, failed SQL or grants that did not verify | Yes |
| `requiere_reinicio` | MariaDB needs a restart for the first TLS; access does not open until resolved | After restarting |
| External access not requested | The engine has anonymous or passwordless accounts the listener would expose | After removing those accounts |

The other causes (IP list, TLS, old credentials, unverified closing) are the same as in [PostgreSQL](https://ardumaker.com/en/docs/postgresql). Follow-up is with `ver_operacion_app` and `reintentar_operacion_app`.

## MCP access

The whole database lifecycle can be operated over MCP with the permissions of the person who authorized the agent. Reference at [/mcp/docs](https://ardumaker.com/en/mcp/docs) and [/mcp/bases-de-datos](https://ardumaker.com/en/mcp/bases-de-datos).

- Read: `ver_catalogo_apps`, `ver_preflight_app`, `listar_apps_empresa`, `ver_app`, `ver_acceso_app` (host, port, TLS and IP list, no secrets), `ver_operacion_app`.
- Modify: `instalar_app` with `app_type: mysql` (requires `idempotency_key`), `cambiar_lista_ip_app` (only if its owner is an admin; IPs must be named explicitly: the agent's IP is not the person's), `reintentar_operacion_app`, `refrescar_apps`.
- Sensitive: `revelar_credencial_app` (admins only, no `idempotency_key`; the answer is marked `sensible: true` and the audit records that it was revealed, never the value).
- Destructive: `revocar_acceso_app` (closes external access for the whole engine) and `desinstalar_app` (with `delete_data` it erases data; requires `confirm_delete_data`).

To read and write data inside the database, the agent uses the [database manager](https://ardumaker.com/en/docs/gestor-de-bases-de-datos) tools. An agent authorized as read-only does not see the tools that modify and cannot reveal credentials.

## Related features

- [Database manager](https://ardumaker.com/en/docs/gestor-de-bases-de-datos/)
- [PostgreSQL](https://ardumaker.com/en/docs/postgresql/)
- [WordPress](https://ardumaker.com/en/docs/wordpress/)
- [Firewall and ports](https://ardumaker.com/en/docs/firewall-y-puertos/)
- [Backups](https://ardumaker.com/en/docs/backups/)
