# How to Run MySQL or MariaDB in a Cloud Sandbox

> Run the official mysql:8.4 or mariadb:11.8 Docker image in a Sandboxer microVM, bound to 127.0.0.1 with --restart unless-stopped. Load the Sakila sample database, then reach it from your laptop with ssh -L through the stanza sbx ssh-config prints. Data survived a stop and resume.

Source: https://sandboxer.com/guides/run-mysql-or-mariadb-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Run the official mysql:8.4 or mariadb:11.8 Docker image in a Sandboxer microVM, bound to 127.0.0.1 with --restart unless-stopped. Load the Sakila sample database, then reach it from your laptop with ssh -L through the stanza sbx ssh-config prints. Data survived a stop and resume.

## What we ran

Everything below ran on production on October 27, 2026, in one `default` sandbox (4 vCPU, 8 GB, Ubuntu 24.04): MySQL Community Server 8.4.13 from the `mysql:8.4` image, then MariaDB 11.8.9 from the `mariadb:11.8` image, both with MySQL's public [Sakila sample database](https://dev.mysql.com/doc/sakila/en/). Timings are single runs or a few back-to-back runs; yours will differ.

| Step | Result |
| - | - |
| `sbx new --type default` | 3.2 s to ready |
| Pull `mysql:8.4` and start it | 20 s (most of it the image pull) |
| Load Sakila (schema + data) | 1.4 s, 16,044 rentals |
| Start MariaDB 11.8 (image already pulled) | 4.9 s to accepting TCP |
| `mysqlslap`, Sakila join, 160 queries, 1 client | 5.1 to 5.6 s (31 to 35 ms a query) |
| Same, 16 clients | 1.6 to 2.9 s |
| Query from a laptop through `ssh -L` | 155 ms median round trip |
| `sbx stop --wait` then `sbx resume` | 2.2 s stop, 1.4 s resume, `memoryRestored: true`, row intact |
| Docker daemon restart | `--restart unless-stopped` brought MySQL back, row intact |

## 1. Start MySQL in a sandbox

Docker is preinstalled in every sandbox. Use the official image as its [Docker Hub page](https://hub.docker.com/_/mysql) documents:

```bash
sbx new --name mysql --type default --no-env
id=sb_7hicnttcab    # the id that sbx new printed

sbx exec $id --timeout 300 "docker run -d --name mysql --restart unless-stopped \
  -e MYSQL_ROOT_PASSWORD=pw -p 127.0.0.1:3306:3306 -v mysqldata:/var/lib/mysql mysql:8.4"
sbx exec $id "until docker exec mysql mysql -uroot -ppw -h127.0.0.1 -e 'select 1' >/dev/null 2>&1; do sleep 0.5; done
docker exec mysql mysql -uroot -ppw -e 'select version()' 2>/dev/null"
```

```text
version()
8.4.13
```

Three choices matter:

- `-p 127.0.0.1:3306:3306`. MySQL listens inside the VM only. Nothing outside can reach it. You connect through SSH (step 4).
- `-v mysqldata:/var/lib/mysql`. The data lives in a named volume on the sandbox disk.
- `--restart unless-stopped`. If the sandbox boots from disk instead of restoring memory, Docker starts MySQL again (step 5).

Wait on a TCP query (`-h127.0.0.1`), not `mysqladmin ping`. On first start, the image runs a temporary server on the socket only, creates the root user, then restarts. Our first script waited on `mysqladmin ping`, which answered from the temporary server, and the next query ran before the real server was up. The logs show the two phases:

```text
[Note] [Entrypoint]: Temporary server started.
[Note] [Entrypoint]: Temporary server stopped
[Server] /usr/sbin/mysqld: ready for connections. Version: '8.4.13'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306
```

## 2. Load the Sakila sample database

Sakila is MySQL's sample DVD-rental schema: films, actors, stores, rentals and payments. Download it from MySQL inside the sandbox and pipe both files into the server:

```bash
sbx exec $id --timeout 300 "curl -fsSLO https://downloads.mysql.com/docs/sakila-db.tar.gz && tar xzf sakila-db.tar.gz
docker exec -i mysql mysql -uroot -ppw < sakila-db/sakila-schema.sql
docker exec -i mysql mysql -uroot -ppw < sakila-db/sakila-data.sql"
```

The load took 1.4 seconds. Check the counts with `COUNT(*)`, not `information_schema.tables.table_rows`: right after the load, `table_rows` is an estimate and left `rental` and `payment` out of our top five.

```text
+---------+----------+-------+
| rentals | payments | films |
+---------+----------+-------+
|   16044 |    16044 |  1000 |
+---------+----------+-------+
```

Now a real query. Save this as `top.sql`. It joins five tables to rank film categories by revenue:

```sql
SELECT c.name AS category, COUNT(*) AS rentals, SUM(p.amount) AS revenue
FROM sakila.payment p
JOIN sakila.rental r ON r.rental_id = p.rental_id
JOIN sakila.inventory i ON i.inventory_id = r.inventory_id
JOIN sakila.film_category fc ON fc.film_id = i.film_id
JOIN sakila.category c ON c.category_id = fc.category_id
GROUP BY c.name ORDER BY revenue DESC LIMIT 5;
```

```bash
sbx scp ./top.sql $id:/home/user/
sbx exec $id "docker exec -i mysql mysql -uroot -ppw -t < top.sql"
```

```text
+-----------+---------+---------+
| category  | rentals | revenue |
+-----------+---------+---------+
| Sports    |    1179 | 5314.21 |
| Sci-Fi    |    1101 | 4756.98 |
| Animation |    1166 | 4656.30 |
| Drama     |    1060 | 4587.39 |
| Comedy    |     941 | 4383.58 |
+-----------+---------+---------+
```

## 3. Measure it with mysqlslap

The `mysql:8.4` image doesn't include `mysqlslap`. Its `/usr/bin` has `mysql`, `mysqladmin`, `mysqldump` and `mysqlsh`, but no `mysqlslap`. Install Ubuntu's client in the sandbox (4.6 s) and point it at the container over TCP:

```bash
sbx exec $id --timeout 300 "sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq mysql-client && mysqlslap --version"
```

```text
mysqlslap  Ver 8.0.46-0ubuntu0.24.04.4 for Linux on x86_64 ((Ubuntu))
```

Save as `slap.sh`. The first run repeats the revenue join, the second uses `mysqlslap`'s own generated mixed read/write load ([mysqlslap options](https://dev.mysql.com/doc/refman/8.4/en/mysqlslap.html)):

```bash
export MYSQL_PWD=pw
mysqlslap -h 127.0.0.1 -uroot --create-schema=sakila \
  --query="SELECT c.name, SUM(p.amount) FROM payment p JOIN rental r USING (rental_id) JOIN inventory i USING (inventory_id) JOIN film_category fc USING (film_id) JOIN category c USING (category_id) GROUP BY c.name" \
  --concurrency=1,4,16 --iterations=3 --number-of-queries=160
mysqlslap -h 127.0.0.1 -uroot --auto-generate-sql --auto-generate-sql-load-type=mixed \
  --concurrency=1,4,16 --iterations=3 --number-of-queries=2000
```

```bash
sbx scp ./slap.sh $id:/home/user/
sbx exec $id --timeout 300 "bash slap.sh"
```

```text
Benchmark
	Average number of seconds to run all queries: 5.057 seconds
	Minimum number of seconds to run all queries: 4.191 seconds
	Maximum number of seconds to run all queries: 5.496 seconds
	Number of clients running queries: 1
	Average number of queries per client: 160
…
Benchmark
	Average number of seconds to run all queries: 1.587 seconds
	…
	Number of clients running queries: 16
	Average number of queries per client: 10
…
Benchmark
	Average number of seconds to run all queries: 0.633 seconds
	…
	Number of clients running queries: 16
	Average number of queries per client: 125
```

We ran the script twice. Averages for all queries in each benchmark:

| Load | 1 client | 4 clients | 16 clients |
| - | - | - | - |
| Sakila revenue join, 160 queries | 5.06 / 5.57 s | 4.51 / 2.36 s | 1.59 / 2.94 s |
| Generated mixed load, 2,000 queries | 1.17 / 1.25 s | 0.72 / 0.63 s | 0.63 / 0.48 s |

The join takes about 31 to 35 ms on one client. More clients help, but the runs vary: 4 clients took 4.5 s in one run and 2.4 s in the next. Run `mysqlslap` several times and compare ranges, not single numbers.

## 4. Connect from your laptop through an SSH tunnel

MySQL listens on `127.0.0.1` inside the VM, so forward a local port to it over SSH. `sbx ssh-config` prints an OpenSSH stanza. You can install it into `~/.ssh/config` with `--install`, or save it to its own file and pass that file with `-F`, which leaves your SSH config untouched:

```bash
sbx ssh-config > sbx_ssh_config
cat sbx_ssh_config
```

```text
# Sandboxer (sbx ssh-config)
Host *.sbx
  User user
  IdentityFile ~/.ssh/sandboxer_ed25519
  IdentitiesOnly yes
  ProxyCommand sbx proxy %h
  UserKnownHostsFile ~/.config/sandboxer/known_hosts
  StrictHostKeyChecking yes
  ServerAliveInterval 30
```

The paths in yours are absolute. Connect once with `sbx ssh` so the CLI authorizes its key on the sandbox, then open the tunnel:

```bash
sbx ssh $id true
ssh -F ./sbx_ssh_config -f -N -o ExitOnForwardFailure=yes -L 13306:127.0.0.1:3306 $id.sbx
```

Any MySQL client on your laptop can now use `127.0.0.1:13306`. We used Python with [PyMySQL](https://pypi.org/project/PyMySQL/) 2.2.8:

```python
import statistics, time
import pymysql

conn = pymysql.connect(host="127.0.0.1", port=13306, user="root", password="pw", database="sakila")
with conn.cursor() as cur:
    cur.execute("SELECT VERSION()")
    print(cur.fetchone()[0])
    times = []
    for _ in range(20):
        t0 = time.perf_counter()
        cur.execute("SELECT title, rental_rate FROM film WHERE title LIKE %s ORDER BY title LIMIT 3", ("AC%",))
        rows = cur.fetchall()
        times.append((time.perf_counter() - t0) * 1000)
    print(rows)
    print(f"median {statistics.median(times):.1f} ms, min {min(times):.1f} ms over {len(times)} queries")
```

```text
8.4.13
(('ACADEMY DINOSAUR', Decimal('0.99')), ('ACE GOLDFINGER', Decimal('4.99')))
median 154.5 ms, min 154.0 ms over 20 queries
```

Almost all of the 155 ms is the network path from our client to the sandbox and back, which depends on where you are. That is fine for a GUI client or a quick check. For load tests, run the client in the sandbox, as in step 3. Close the tunnel by killing that `ssh` process.

[`sbx forward $id --remote 3306`](/docs/ssh#forward-ports) is the CLI's built-in alternative to `ssh -L`. We tested the `ssh -L` route only.

## 5. What survives a stop and resume

We inserted a row, stopped the sandbox and resumed it:

```bash
sbx exec $id "docker exec mysql mysql -uroot -ppw -e \"INSERT INTO sakila.actor (first_name, last_name) VALUES ('SANDBOX', 'TEST')\""
sbx stop $id --wait
sbx resume $id
sbx exec $id "docker ps --format '{{.Names}} {{.Status}}'
docker exec mysql mysql -uroot -ppw -N -e \"SELECT actor_id, first_name, last_name FROM sakila.actor WHERE last_name = 'TEST'\""
```

```text
mysql Up 3 minutes
201	SANDBOX	TEST
```

The stop took 2.2 seconds and the resume 1.4 seconds. The resume reported `memoryRestored: true`, so MySQL never restarted (`Up 3 minutes`).

A resume boots from disk instead when no machine with the same CPU type is free, or when you resize. Then the disk is kept and processes start fresh: the Docker volume is there, and the restart policy decides whether MySQL comes back. We didn't force a boot from disk here. Restarting the Docker daemon shows the same policy at work:

```bash
sbx exec $id "sudo systemctl restart docker; sleep 1
until docker exec mysql mysql -uroot -ppw -h127.0.0.1 -e 'select 1' >/dev/null 2>&1; do sleep 0.5; done
docker ps --format '{{.Names}} {{.Status}}'
docker exec mysql mysql -uroot -ppw -N -e \"SELECT actor_id, first_name, last_name FROM sakila.actor WHERE last_name = 'TEST'\""
```

```text
mysql Up 1 second
201	SANDBOX	TEST
```

See [snapshots](/docs/snapshots#resume) for the resume rules. The [pgvector guide](/guides/run-postgres-with-pgvector-in-a-sandbox) forces a boot from disk with a resize and shows a database coming back the same way.

## 6. MariaDB instead

The [official MariaDB image](https://hub.docker.com/_/mariadb) works the same way. Its variables start with `MARIADB_`, and the client is `mariadb`. We ran it next to MySQL on host port 3307 in the same sandbox:

```bash
sbx exec $id --timeout 300 "docker run -d --name maria --restart unless-stopped \
  -e MARIADB_ROOT_PASSWORD=pw -p 127.0.0.1:3307:3306 -v mariadata:/var/lib/mysql mariadb:11.8
until docker exec maria mariadb -uroot -ppw -h127.0.0.1 -e 'select 1' >/dev/null 2>&1; do sleep 0.5; done
docker exec maria mariadb -uroot -ppw -N -e 'select version()'
docker exec -i maria mariadb -uroot -ppw < sakila-db/sakila-schema.sql
docker exec -i maria mariadb -uroot -ppw < sakila-db/sakila-data.sql
docker exec -i maria mariadb -uroot -ppw -t < top.sql"
```

```text
11.8.9-MariaDB-ubu2404
+-----------+---------+---------+
| category  | rentals | revenue |
+-----------+---------+---------+
| Sports    |    1179 | 5314.21 |
…
```

MariaDB took 4.9 seconds to accept TCP connections (image already pulled), loaded Sakila without errors and returned the same revenue table. Unlike `mysql:8.4`, the `mariadb:11.8` image ships `mariadb-slap`, so you can benchmark without installing anything. For the tunnel, forward to port 3307 instead of 3306.

## 7. Clean up

```bash
sbx delete $id --yes
```

A `default` sandbox costs $0.036/hour while it runs and nothing while it is stopped.

## FAQ

### Why does my first query fail right after `docker run`?

On first start, the `mysql:8.4` image runs a temporary server on the socket only, then restarts it with networking. `mysqladmin ping` can answer from the temporary server. Wait until a TCP query with `-h127.0.0.1` succeeds instead; we used the same wait for MariaDB.

### Where is mysqlslap in the MySQL Docker image?

The `mysql:8.4` image doesn't include it. Install `mysql-client` from Ubuntu in the sandbox (it ships `mysqlslap` 8.0.46) and connect over TCP to `127.0.0.1:3306`, or use `mariadb-slap`, which is in the `mariadb:11.8` image.

### Can I give MySQL a public URL instead of a tunnel?

A [hosted URL](/docs/hosting) is HTTPS, built for web apps and APIs. A MySQL client speaks the MySQL protocol on a TCP port, so we used SSH and didn't test a hosted URL. The tunnel also keeps the database private: MySQL listens only on `127.0.0.1` in the VM.

### Will my data survive if the sandbox auto-stops?

Yes. Auto-stop is a normal stop: the disk and memory are saved, and `sbx resume` brings the sandbox back. In our run, MySQL kept running through the stop and the row was there. Keep `--restart unless-stopped` for the times a resume boots from disk.

### MySQL or MariaDB?

Use the one you run in production. Both loaded Sakila and returned the same results here. They differ in features and in later versions, so test against the engine and major version your app uses.

## Related

- [Run Postgres with pgvector in a sandbox](/guides/run-postgres-with-pgvector-in-a-sandbox)
- [Test text-to-SQL queries against a real Postgres](/guides/test-text-to-sql-queries-against-a-real-postgres)
- [SSH into a cloud sandbox from VS Code or Cursor](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor)
- [SSH and file transfer](/docs/ssh)
