Skip to content

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 }
KeyMeaning
enginepostgres, mysql or redis
versionMajor, or major.minor: "18", "18.2". Minimum Postgres 18, MySQL 8, Redis 7.
extensionsPostgres only: pgvector, postgis, pg_cron, timescaledb
resourcescpu, memory, disk (the data volume quota), as for services
backup.scheduleFive-field cron expression, UTC
backup.retainNumber 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 client

The 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 ​

EngineBackup
PostgresBase backup plus continuous WAL archive: restore to any moment
MySQLmysqldump
RedisRDB 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 restores

A 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
EngineAccepted
PostgresPlain SQL or pg_dump -Fc, optionally gzipped
MySQLmysqldump SQL, optionally gzipped
Redisdump.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.

Duploy — our nodes, your file.