---
title: "Postgres, MySQL & MongoDB"
description: "Run a managed Postgres, MySQL, or MongoDB database for your app that sleeps when idle, backs itself up continuously, and connects over TLS."
---

A dply database is a Postgres, MySQL, or MongoDB server that belongs to one app. dply starts it, puts its address and password in the app's environment on the next deploy, backs it up continuously, and lets it sleep when nothing is connected so you pay for compute only while it is used. Use one when your app needs a real relational or document database. For small, read-heavy data at the edge, see [Edge SQL (D1)](/docs/resources/sql) instead.

> [!NOTE]
> A database needs a card on the account. It is billed to that card as usage. SQLite does not need one.

## Engines

| Engine | Version | Port | Login | Database |
|---|---|---|---|---|
| Postgres | 17 | `5432` | `app` | `app` |
| MySQL | 8.4 | `3306` | `app` | `app` |
| MongoDB | 7 | `27017` | `app` | `app` |
| SQLite | | | | a file in the app |

Every server engine accepts TLS connections only. Plain-text connections are refused.

SQLite is a file at `/tmp/database.sqlite` inside the app. It is saved while the app runs and restored when the app wakes, and one instance serves the app so the file stays consistent. Migrations run each time the app starts. It has no backups, statistics, or console.

### Manage SQLite

Choose the **SQLite** card on the app's map. Its sheet shows:

- **The file**: where it is in the app, the env the app gets, and the size and time of the last saved copy. While the app runs, it copies the file to dply's object storage every 20 seconds, and it puts that copy back when it wakes. Writes made in the last few seconds before the app sleeps can be lost, so use a dply database for data you can't afford to lose. **Download a copy** gives you that file (a link that works for 10 minutes; it needs permission to change the app).
- **Tools**, for Laravel and Rails apps: **Migrate**, **Status**, **Seed**, **Prepare** (Rails) and **Roll back**, run in the live app. If the running app still has another database from its last deploy (say you just detached Postgres), they tell you to redeploy instead of running.
- **Add a database**, for data that several instances or queue workers share. The first one you add becomes the app's primary and takes over `DB_*`.

## Create a database

1. In your app, open **Overview**.
2. Choose **Add resource**, then **Database**.
3. Under **Create new**, pick **Postgres**, **MySQL** or **MongoDB**, and enter a **Name** (letters, numbers and dashes). Or choose **Attach existing** to use a database your organization already has.
4. Choose **Create database** (or **Attach**). A card for it appears on the app's map. It starts at the smallest size, a 1 GB disk and 5 minutes' sleep; change those on its card (see below).
5. Deploy the app.

The database starts on its first connection. The first start creates its disk and takes a few seconds, once. After that the app receives these values on every deploy:

| Engine | Environment variables set by dply |
|---|---|
| Postgres | `DB_CONNECTION=pgsql`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `DB_SSLMODE=require`, `DATABASE_URL` |
| MySQL | `DB_CONNECTION=mysql`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `MYSQL_ATTR_SSL_CA`, `DATABASE_URL` |
| MongoDB | `MONGODB_URI`, `MONGO_URL`, `MONGODB_DATABASE` |
| SQLite | `DB_CONNECTION=sqlite`, `DB_DATABASE=/tmp/database.sqlite` |

A value you save yourself in [Environment variables](/docs/environment-variables) wins over the one dply sets.

An app can have several databases. The first is its **primary** and uses the names above. Each other database uses the same names behind its own prefix, taken from its name: a database named `analytics` sets `ANALYTICS_DB_HOST`, `ANALYTICS_DB_PASSWORD`, `ANALYTICS_DATABASE_URL` and the rest (`ANALYTICS_MONGODB_URI` for MongoDB), and its card shows a Laravel connection that reads them. Migrations on deploy run against the primary. Choose **Make primary** on another database's card to swap which one uses the plain names; the old primary moves to its own prefix. Preview deployments never receive any of these values.

Databases are available to apps with server code: container apps, and SSR or hybrid apps. On an SSR or hybrid app the values arrive as Worker bindings (`env.DATABASE_URL`), and the Worker needs a driver that opens TCP sockets with the `nodejs_compat` flag. A [database pool](/docs/resources/database-pools) in front of the database saves the connection setup on every request.

## Sizes and disk

| Size | Memory | Compute units |
|---|---|---|
| 0.25 vCPU | 1 GB | 0.25 |
| 0.5 vCPU | 2 GB | 0.5 |
| 1 vCPU | 4 GB | 1 |
| 2 vCPU | 8 GB | 2 |
| 4 vCPU | 16 GB | 4 |

The 1, 2 and 4 vCPU sizes appear in the size list once dply enables the larger database nodes. Until then only 0.25 and 0.5 vCPU can be picked. These sizes always stay on: they do not sleep when idle, and picking one sets **Sleep** to **Stays on**. Moving back to 0.25 or 0.5 vCPU gives you the sleep choice again. A new large database can take a few minutes to start the first time, while a node is added for it.

Disks come in `1 GB`, `3 GB`, `5 GB`, `10 GB`, and `25 GB`. You can change the size and the disk later from the same sheet. A new size applies the next time the database wakes. A disk only grows: picking a smaller one is refused.

Each size is tuned for its memory. Postgres gets a quarter of it for shared buffers and caps the write-ahead log at a quarter of the disk. MySQL gives the buffer pool half the memory. MongoDB sizes the WiredTiger cache to the plan.

### Suggested resizes

dply watches each database's memory and how many reads come from memory, and suggests a different size when it would help. It never resizes on its own, because a resize restarts the database.

- **Bigger**: when the database has held over 85% of its memory, or served under 90% of reads from memory, for the last 6 hours it ran.
- **Smaller**: when it has used under 35% of its memory for 7 days, with nearly every read from memory.

Every database on the app gets its own suggestions, the primary and the others. The suggestion appears at the top of the database's sheet with the reason and the price change, and you get a notification that links to it. Choose one:

- **Resize now**: the database restarts at the new size the next time the app connects. Open connections drop once.
- **Resize tonight**: at 03:00 in your organization's time zone. Choose **Cancel** on the sheet to undo it before then.
- **Dismiss for 30 days**: the same suggestion stays hidden for 30 days.

Only people who can change the app see these buttons.

## Sleep and wake

Pick how long the database waits after the last connection closes before it sleeps: **1 minute**, **5 minutes**, **15 minutes**, or **Stays on**. A sleeping database stops its compute and keeps its data on its disk. The next connection wakes it in about a third of a second; the client waits while it does.

Compute is billed only while the database is awake. The disk is billed whether it is awake or asleep. **Stays on** bills every hour.

Anything that keeps a connection open keeps the database awake. A queue worker polling a `database` queue is the common case. Put queues on [Valkey](/docs/resources/valkey) if you want the database to sleep.

## Connect

Open the database card, then the **Connect** tab. It shows the **Host**, **Port**, **User**, **Password** (choose **Show**), a **Connection URL**, and a command to open a shell. The host has the form `<id>.db.dply.io`.

### Laravel

The next deploy sets `DB_CONNECTION`, the host, the password, and `DATABASE_URL`, so the stock `config/database.php` works unchanged. To run migrations on each start, open **Sleep, scaling, region** on the app card and turn on **Run migrations when a container starts** (`migrate --force --isolated`).

```php
// config/database.php works as shipped. For Postgres, dply-edge also sets:
// DB_SSLMODE=require
$users = DB::table('users')->count();
```

#### Database pooler

When the database is far from the app, opening a new TLS connection to it can add a few hundred milliseconds to every request. For a Laravel app on a dply Postgres, open **Sleep, scaling, region** and turn on **Database pooler (PgBouncer)**. Apps created since 2026-11-06 start with it on. After the next deploy, each container runs PgBouncer, and requests connect to it locally while it keeps the connections to the database open. The app's `DB_HOST`, `DB_PORT`, `DB_SSLMODE` and `DATABASE_URL` point at PgBouncer automatically; nothing in `config/database.php` changes.

- PgBouncer runs in session mode, so transactions, advisory locks and `LISTEN` work as before.
- Each instance keeps at most 8 connections open. Postgres allows 50 in all, and dply keeps 5 free and leaves room for your queue workers, so an app that can run many instances gets fewer per instance. A request that finds every connection busy waits for one.
- It applies to FrankenPHP and php-fpm. Worker mode (Octane), queue workers and all-in-one apps keep their connections open, so they connect directly.
- PgBouncer listens on `127.0.0.1` without TLS. If your `config/database.php` hard-codes `'sslmode' => 'require'` (or `verify-ca`, `verify-full`), the deploy leaves the pooler off and says so in the build log. Change it to `env('DB_SSLMODE', 'prefer')` (the stock value) and redeploy.
- To see the pool, open the app and use **Check now** next to where it runs. It shows how many of the instance's database connections are busy and idle, and how many requests are waiting for one. Requests that keep waiting mean the pool is too small: lower the app's maximum instances or move queue workers to fewer processes.
- The database still sleeps when the app is idle, and the next request wakes it through PgBouncer.
- Connecting through PgBouncer takes about a millisecond. dply also skips Laravel's `set search_path to "public"` on every connect to a dply Postgres (pooled or not), since the database already defaults to `public`; a `search_path` you set yourself is still sent.
- Each query still makes one round trip to the database. Only placing the app near its database shortens that.

### Node

```js
// Postgres
import pg from 'pg';
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();

// MongoDB
import { MongoClient } from 'mongodb';
const mongo = new MongoClient(process.env.MONGODB_URI);
const db = mongo.db(process.env.MONGODB_DATABASE);
```

### A shell

```bash
# Postgres
psql "postgresql://app@<id>.db.dply.io:5432/app?sslmode=require"

# MySQL: the mysql client needs the host as the TLS server name
mysql -h <id>.db.dply.io -P 3306 -u app -p --ssl-mode=REQUIRED --tls-sni-servername=<id>.db.dply.io app
```

The shell asks for the password from the **Connect** tab. Connecting wakes the database if it is asleep.

> [!IMPORTANT]
> The database address is public, protected by TLS and the password. Your client must send the host name (SNI) during the TLS handshake. Connecting by IP address does not work.

### Read-only login

For Postgres and MongoDB, **Connect** can create a second login, `app_ro`, that can read every table and change nothing. Use it for BI tools such as Metabase. Choose **New password** to create or rotate it; the password is shown once. **Turn off** removes it.

### MySQL latency

MySQL sends each parameterised query in two round trips. Set `DPLY_MYSQL_ONE_ROUND_TRIP=true` in Environment and redeploy to send each in one; parameters are then escaped into the query by PHP instead of bound by MySQL.

## Region

Every database runs in the region shown on the sheet (currently New York). Container apps that use a dply database run near it by default. The **Overview** tab shows the measured round trip from the app at its last deploy. See [Data regions](/docs/data-regions).

## Backups and restore

Every change is streamed to storage, and a full backup runs daily. Backups are kept for 7 days on Starter, 14 on Pro and 30 on Team, and a weekly check restores each Postgres database from its backups to prove they work. This works the same for all three engines: Postgres uses its write-ahead log, MySQL its binlog, and MongoDB its oplog.

To restore:

1. Open the database card, then the **Backups** tab.
2. Under **Restore to a point in time**, enter a **Time (UTC)** or use a **Quick pick**.
3. Choose **Restore** and confirm.

Restoring replaces the data with how it was at that second. It takes a few minutes. The app keeps its address and password. The **Backups** tab also shows when the last full backup ran and whether saving recent changes is healthy.

To look before you replace anything, enter a name under **Or restore into a new database named** and choose **Restore as new**. The data as it was at that time goes into a new database of the same engine, size and disk, and the original is left as it is. The copy is attached to the app with its own env prefix, so detach it if the app should not use it, and delete it when you are done: it bills like any database. Changes from the last minute or so before the time you pick may not have reached the backups yet.

### Export and import

Under **Export and import**, **Export now** writes a full copy of the data as one file and keeps it beside the backups. **Download** saves it; **Load** loads it back.

| Engine | Export format | Import your own dump |
|---|---|---|
| Postgres | `pg_dump -Fc` | Yes, a `pg_dump -Fc` file |
| MySQL | `mysqldump`, gzipped | No |
| MongoDB | `mongodump --archive` | Yes, a `mongodump --archive` file, gzipped or not |

To import, choose **Get upload command**, run it where the file is (the link works for an hour), then choose **Load it**. For Postgres, tables in the file are replaced and others are left alone. For MongoDB, everything in the database is replaced.

## Look inside

The database sheet has tabs for day-to-day work. None of them wakes the database until you load or refresh something. For a database that isn't the primary, open its card and choose **Stats, console & backups**: the same tabs, apart from **Connect** and **Settings**, which are on its card.

- **Overview**: state, size, disk use, awake hours this month, and round-trip time from the app.
- **Statistics**: data size, tables, rows, connections, cache hit rate, and the largest tables. **Load live stats** wakes the database.
- **Queries**: the queries that took the most total time, and what is running now. You can cancel a running query.
- **Health**: disk use, tables that probably need an index, indexes nothing uses, dead rows, and Postgres extensions (**Turn on**).
- **Console**: one read-only statement at a time, up to 200 rows and 15 seconds.
- **Settings**: **Migrate**, **Seed**, **Prepare**, and **Roll back**, and **Run migrations when a container starts**.

## Pricing

Compute is billed per second while awake. Storage is billed per GB-month on the disk size you picked, awake or asleep.

<!-- generated: php artisan dply:billing:price-table sizes --product=database -->
| Size | Memory | Per second awake | Per hour awake |
| --- | --- | --- | --- |
| 0.25 vCPU | 1 GB | $0.00000833 | $0.03 |
| 0.5 vCPU | 2 GB | $0.0000167 | $0.06 |

The larger sizes, once enabled, stay on all month: 1 vCPU is $0.18 an hour (about $129.60 a month), 2 vCPU $0.24 (about $172.80) and 4 vCPU $0.48 (about $345.60), plus the disk.

Storage, and compute per compute unit (the 0.25 vCPU size is a quarter of one):

<!-- generated: php artisan dply:billing:price-table rates --group="Databases" -->
| Meter | Price | Unit |
| --- | --- | --- |
| Compute | $0.0000333 | per compute-unit-second (1 vCPU, 4 GB) |
| Storage | $0.20 | per GB-month |

A 0.25 vCPU database that stays on all month (720 hours) with a 1 GB disk is about $21.80. The same database awake 8 hours a day is about $7.40. This bills at the invoice line **Databases**. Usage shows on the app's bill and in [Usage & metering](/docs/usage).

### What each database's card shows

Each database's card has its own **Tools** (Laravel apps): **Migrate**, **Status**, **Seed** and **Roll back** run with `--database` set to that database, using its prefixed env. Laravel runs every migration against the connection you give it, so set `$connection` on the migrations that belong to it. The app needs dply/laravel 1.2 or later for this.

Every database has everything on this page: statistics, the console, backups and restore, export and import, suggested resizes, and the disk, connection, memory and backup alerts. The primary's card opens them from **Stats & backups**. Another database's card shows its disk use, connections and last backup, updated hourly without waking it, and opens the rest from **Stats, console & backups**. Its awake time and storage are its own; the primary's **Awake this month** counts only the primary.

## From the API and the CLI

The [API](/docs/api/reference#app-databases) and the [CLI](/docs/cli#an-apps-databases) list an app's databases and, for any of them, run a read-only query, start and list exports, and restore to a point in time:

```sh
dply-edge edge databases list
dply-edge edge databases query reports "select count(*) from orders"
dply-edge edge databases export reports
dply-edge edge databases restore reports "2026-09-28 14:30"
dply-edge edge databases restore reports "2026-09-28 14:30" --as-new reports-check
```

Creating, attaching, resizing and deleting a database are in the dashboard.

## Detach or delete a database

Open the database's card, then under **Manage**:

- **Detach** takes it off this app. The database, its data and its bill stay in your organization, so you can attach it to another app, or back to this one, with **Add resource** → **Database** → **Attach existing**. When you detach the primary, the next database becomes primary; with none left, the app goes back to SQLite.
- **Delete** destroys it. Type its name to confirm. A database attached to other apps can't be deleted until you detach it there.

### Databases no app uses

A detached database keeps running and billing for its disk. **Projects** → **Databases** lists every Postgres, MySQL and MongoDB database in your organization, with the apps using each one. From there you can:

- **Attach** it to an app. Tick **as its primary** to make it the app's `DB_*` connection.
- **Detach** it from an app.
- **Delete** it, once no app uses it.

Redeploy the app after attaching so it picks up the connection.

To move to another engine, add a database with the new engine, move your data, make it primary, then delete the old one.

> [!WARNING]
> Deleting a database deletes its disk and all of its backups at once. It cannot be undone. Choose **Export now** and **Download** first if you want to keep the data.

## Related

- [Database pools](/docs/resources/database-pools)
- [Valkey (Redis)](/docs/resources/valkey)
- [Data regions](/docs/data-regions)
- [Usage & metering](/docs/usage)
