# MySQL

A managed MySQL 8.4 for your apps: one server, backed up every night, attached to your apps as `MYSQL_URL`.

## What you get

Each MySQL is MySQL Community Server 8.4 LTS, from the unmodified upstream image. It is a **single instance**: one server, with no live copy on another server (see [Availability](#availability)). It has one database, `app`, and one user, `app`, with all rights on that database and nothing else. The server's character set is utf8mb4. It runs in its own microVM, reachable only by the apps you attach it to; it has no public address.

```sh
$ deplane mysql create shop
$ deplane mysql attach shop --app web
```

The first start takes about 30 seconds. An app has one MySQL, a MySQL may serve several of your apps (and their workers and cron jobs), and an app may also have a Postgres [database](databases.md) and a [Redis](redis.md). Pull request previews do not get the MySQL.

In the dashboard, open **Databases** and find **MySQL**.

## Sizes

| Size | Memory | CPU | Buffer pool | Connections | Storage |
|---|---|---|---|---|---|
| medium (default) | 1 GB | 1 vCPU | 384 MB | 100 | 10 GB |
| large | 2 GB | 2 vCPU | 1 GB | 200 | 10 GB |

Pick the size when you create it: `deplane mysql create shop --size large`. A MySQL counts as one instance of its size against your plan, like an app instance. No small size: MySQL 8.4 and its microVM need more than 512 MB. In our tests a medium MySQL used 243 MB at rest and at most 682 MB with 16 busy clients on 920 MB of data.

## Connect

Attaching sets `MYSQL_URL` on the app and rolls out a new release with it:

```
mysql://app:PASSWORD@my--NAME:3306/app
```

The parts:

| Part | Value |
|---|---|
| Host | `my--NAME`, for example `my--shop` |
| Port | 3306 |
| User | `app` |
| Password | 32 random letters and digits, only inside `MYSQL_URL` |
| Database | `app` |

Deplane shows the password nowhere else. Many libraries take the URL as it is, for example `mysql.createConnection(process.env.MYSQL_URL)` with Node's mysql2. If your app wants the parts as separate settings, read them from `MYSQL_URL` when it starts. A [template](templates.md) can set them for you with `${mysql.url}`, `${mysql.host}`, `${mysql.port}`, `${mysql.user}`, `${mysql.password}` and `${mysql.database}`.

A template that needs a MySQL creates and attaches one for you, named after the app (`APP-mysql`), at the size the template asks for (medium unless it says large). Deleting the template deploy ("Delete all of it") destroys that MySQL with its data, unless another app uses it by then.

A client on your own computer (MySQL Workbench, TablePlus, the `mysql` command) cannot connect, because the MySQL has no public address. Run SQL through one of your apps with [`deplane run`](cli.md#deplane-run) instead. Most app images have no MySQL client: add one to your Dockerfile (`apk add mysql-client` on Alpine, `apt-get install -y default-mysql-client` on Debian), or use your app's own language. With a client in the image:

```sh
$ deplane run --app web -- sh -c 'p=${MYSQL_URL#mysql://app:}; MYSQL_PWD="${p%%@*}" mysql -h my--shop -u app app -e "SHOW TABLES"'
```

`deplane run` shows only the first 10 MB of output, so use it for queries and small exports, not for copying a large database.

## Client notes

MySQL 8.4 signs users in with `caching_sha2_password`. Current clients do this without TLS: in our tests PHP's mysqlnd (the WordPress image) and Node's mysql2 (the Ghost image) both connected over a plain connection, also right after a restart. So the older `mysql_native_password` is off. A client that knows only that older method cannot sign in; update the client.

A few clients need a setting to sign in without TLS:

- Java (Connector/J) with `useSSL=false`: add `allowPublicKeyRetrieval=true`.
- Python's PyMySQL: install the `cryptography` package.

Traffic to the MySQL never leaves Deplane's private network, so TLS is not needed. It is available: the server has its own self-signed certificate. Use TLS without verifying the certificate (for example `ssl-mode=REQUIRED`); a check of the certificate against a known authority fails. Deplane does not manage MySQL certificates.

## The app password

Keep the `app` password as it is: `MYSQL_URL` depends on it. If you change it (with `SET PASSWORD`, or a plugin or tool that does), your apps cannot connect with `MYSQL_URL` until the MySQL restarts. If you change it, Deplane sets it back at the next restart; a restore restarts it too. Backups and health checks sign in with a separate account of Deplane's, which can read your data for dumps but cannot write rows, so they keep working.

## Backups and restore

Every MySQL is backed up every night, at a time of its own between 01:00 and 05:00 UTC. A backup is a full copy of the `app` database (tables, views, routines, triggers and events), taken while the MySQL keeps running, compressed, encrypted, with an off-site copy kept 7 days. Use InnoDB tables (the default): other kinds of tables are copied without a consistent snapshot.

Back it up now with `deplane mysql backup NAME` (once an hour, or 5 minutes after a backup failed) and list backups with `deplane mysql backups NAME`. Backups are kept for the backup window: 3 days, or your plan's backup retention if that is shorter. The newest backup that worked is kept longer, until a newer one works.

`deplane mysql restore NAME BACKUP-ID` replaces all the data of the MySQL `NAME` with a backup. It asks for your passkey. The backup can be from any MySQL of your team: its own, another one, or one you destroyed (list those with `deplane mysql backups --destroyed`). The MySQL stops while the restore runs, so its apps cannot connect, and it starts again by itself; data written since the backup is lost. The `app` password stays the MySQL's own, so `MYSQL_URL` keeps working.

How long it takes: in our tests, 920 MB of data backed up in 17 seconds (244 MB compressed) and loaded back in under 2 minutes. A full 10 GB takes about 3 minutes to back up and about 20 minutes to restore.

If a restore fails after it started replacing the data, the MySQL may start empty: restore again or email support@deplane.cloud. A restore that fails before then (the backup could not be downloaded, for example) leaves the data unchanged, and its message says so.

## How much you can lose

A MySQL is backed up once a night. If its data is lost, you can lose up to about a day of data: there is no point-in-time restore. Back it up now before a risky change, such as a migration or an import. If you need restore to any second, use a Postgres [database](databases.md).

## Availability

A MySQL is a single instance. If the server holding its disk goes down, the MySQL is down until that server returns; its apps cannot reach it meanwhile. If that server is lost, restoring a backup is the way out: create a new MySQL, restore the old one's latest backup into it, and attach your apps. This is the same promise as [Redis](redis.md) and [app disks](disks.md). It is below Postgres [databases](databases.md#live-copy-and-failover), which keep a live copy on another server.

## When the disk fills

A MySQL has a 10 GB disk for its data, its indexes and its temporary files, and it does not grow. When it is full, writes fail or wait, and large queries that need temporary space fail; reads keep working. Delete data you no longer need, then run `OPTIMIZE TABLE` on the tables you emptied to give the space back. To see how much the data takes:

```sql
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024) AS mb
FROM information_schema.tables WHERE table_schema = 'app';
```

The nightly backup keeps working while the disk is full.

## Limits

- Sizes: medium (1 GB) or large (2 GB), each counted as one instance of its size in your plan's instances and memory. See [invalid_mysql_size](errors.md#invalid_mysql_size), [limit_size](errors.md#limit_size), [limit_instances](errors.md#limit_instances) and [limit_memory](errors.md#limit_memory).
- Storage: 10 GB.
- MySQL per team: your plan's (Beta: 1). See [limit_mysql](errors.md#limit_mysql).
- MySQL per app: 1. See [mysql_attached_elsewhere](errors.md#mysql_attached_elsewhere).
- One database and one user (`app`). There is no root access, and no replicas, binary logs or plugins.
- A MySQL uses one of the platform's database disks, which it shares with Postgres. If none is free, creating one is refused with [database_capacity](errors.md#database_capacity). If the servers are nearly full, it is refused with [capacity_full](errors.md#capacity_full).
- An unknown name, or another team's MySQL, gets [mysql_not_found](errors.md#mysql_not_found).
- An [AI tool](mcp.md) can list, create, attach, detach and back up a MySQL. Destroying or restoring one, listing a destroyed MySQL's backups, and setting `MYSQL_URL` by hand ([mcp_mysql_url](errors.md#mcp_mysql_url)) are for people only.

See also [Limits](limits.md#mysql).

## Destroy

`deplane mysql destroy NAME` removes `MYSQL_URL` from its apps and deletes the MySQL and its data. It asks for your passkey. Its backups are kept for the backup window (3 days at most), then deleted. Until then you can restore them into another MySQL: `deplane mysql backups --destroyed` lists them.

MySQL is a trademark of Oracle and/or its affiliates. Deplane is not affiliated with Oracle.
