Appearance
Managed databases
toml
[databases.db]
engine = "postgres"
version = "18"
extensions = ["pgvector"]
resources = { cpu = 1.0, memory = "1G", disk = "20G" }
backup = { schedule = "0 3 * * *", retain = 7 }| Key | Meaning |
|---|---|
engine | postgres, mysql or redis |
version | Major, or major.minor: "18", "18.2". Minimum Postgres 18, MySQL 8, Redis 7. |
extensions | Postgres only: pgvector, postgis, pg_cron, timescaledb |
resources | cpu, memory, disk (the data volume quota), as for services |
backup.schedule | Five-field cron expression, UTC |
backup.retain | Number of base backups to keep |
Each database is one container with a persistent volume, reachable inside the project as <name>.<project>.internal. Services get its credentials through from_database. Changing the major version of an existing database is refused, in both directions.
Extensions are created once
The listed extensions are created when the database is first initialised. Adding one to a database that already exists is not applied automatically.
Connecting
bash
duploy db creds db # credentials
duploy db connect db # psql through a tunnel
duploy db connect db -- -c 'select now()'
duploy tunnel db:db # local port for your own clientThe tunnel goes through the control plane; the database is never exposed to the internet. db connect runs psql, mysql or redis-cli from your PATH. Point it elsewhere with DUPLOY_PSQL, DUPLOY_MYSQL or DUPLOY_REDIS_CLI.
Backups and restore
| Engine | Backup |
|---|---|
| Postgres | Base backup plus continuous WAL archive: restore to any moment |
| MySQL | mysqldump |
| Redis | RDB snapshot |
bash
duploy db backup db # take one now
duploy db backups db
duploy db restore db --backup <id>
duploy db restore db --at 2026-10-20T14:05:00Z # Postgres: point in time
duploy db timelines db # Postgres timelines after restoresA restore happens in place: the database stops, the current data is copied aside, the backup is restored, and the copy is put back if anything fails.
Importing a dump
duploy db import replaces the database with a dump:
bash
duploy db import db ./dump.sql.gz| Engine | Accepted |
|---|---|
| Postgres | Plain SQL or pg_dump -Fc, optionally gzipped |
| MySQL | mysqldump SQL, optionally gzipped |
| Redis | dump.rdb, optionally gzipped |
Directory-format dumps (pg_dump -Fd) are rejected; dumps taken with --create fail. An import uses the same path as a restore, so a failed import leaves the old data in place.
A Postgres dump is loaded as the app user, never as a superuser. Lines that need a superuser are skipped and counted in the job log rather than failing the import: comments on extensions, ownership changes (objects end up owned by app), and the rows of extension-owned tables such as cron.job. Re-create pg_cron jobs with cron.schedule after an import. A dump that needs an untrusted extension your config does not list fails with permission denied to create extension; add it to extensions.
Postgres roles and row-level security
The app user is the owner of the database, but not a superuser: NOSUPERUSER NOBYPASSRLS CREATEDB CREATEROLE. Row-level security policies apply to it.
A table's owner bypasses RLS unless the table has FORCE ROW LEVEL SECURITY. If your app relies on RLS, let it connect as its own role. app can create one, but since PostgreSQL 16 a role created by a non-superuser does not grant its creator SET. Ask for it in the same session:
bash
duploy db connect db -- \
-c "SET createrole_self_grant = 'set'" \
-c "CREATE ROLE myapp LOGIN PASSWORD '…' NOSUPERUSER NOCREATEROLE" \
-c "CREATE DATABASE myapp OWNER myapp"Then point the app at it with a secret, for example postgres://myapp:…@db.shop.internal:5432/myapp.