Databases, backups and restores
Most apps need a database, and a database needs credentials, a data volume,
a health check and backups. isb makes one a single command: a database is an
app whose source is a database engine instead of an image
or a repository. It lives in a project environment like any app, runs as one
service of the stack <project>-<env>, and other apps reach it by service
name. Its backups stream straight from the database to an S3-compatible
bucket.
$ isb project create shop
$ isb db create pg --project shop --engine postgres # or postgres:16
created database pg
engine: postgres 17
host: pg.shop-production (pg.shop-production.acme.isb)
port: 5432
user: pg
database: pg
password: secret db.pg.password (isb secret get db.pg.password / --show-password)
url: postgres://pg:${{secret.db.pg.password}}@pg.shop-production:5432/pg
for apps: DATABASE_URL=${{secret.db.pg.url}}
volume: shop-production_pg_data
$ isb app create web --project shop --image docker:myapp:1 -e 'DATABASE_URL=${{secret.db.pg.url}}' --deploy
isb db create NAME --project P [--environment E] --engine ENGINE[:VERSION] [--database D] [--user U]
[--publish [IP:]PORT] [--no-deploy] postgres, mysql, mariadb, mongodb, redis
isb db ls [--project P] [--json] | show NAME [--show-password] [--json] | rm NAME
All take --org ORG. In the web UI: New database from a project's menu,
and the database's Database and Backups tabs
(The web UI).
Engines#
| Engine | Image (default tag) | Port | Data | Health check |
|---|---|---|---|---|
postgres |
docker:postgres:17 |
5432 | /var/lib/postgresql/data (PGDATA a subdirectory) |
pg_isready |
mysql |
docker:mysql:8.4 |
3306 | /var/lib/mysql |
mysqladmin ping |
mariadb |
docker:mariadb:11.4 |
3306 | /var/lib/mysql |
mariadb-admin ping |
mongodb |
docker:mongo:8.0 |
27017 | /data/db |
mongosh ping |
redis |
docker:redis:7.4 |
6379 | /data (RDB snapshots, --save 60 1) |
redis-cli ping |
--engine ENGINE:TAG (version in the tools) picks another tag of the
official image. The deploy pins the tag to its digest, as for image apps.
What a database gets that an image app does not:
Its data on a named volume,
<stack>_<db>_data, mounted at the engine's data directory. Deleting the database keeps the volume.One replica, rolled out stop-first. Two live copies of a database on one data directory corrupt it, so
replicasis at most 1.A health check per engine, with a two-minute start period for the first initialization. A deploy is done when the check passes.
Credentials, generated on create, kept as org secrets and delivered as
{secret: NAME}environment (on OCI images that is incus instance config: the documented trade-off in Secrets):Secret Holds db.<name>.passwordThe user's password (Redis: requirepass; MongoDB: the root user's).db.<name>.root-passwordMySQL and MariaDB: root's password.db.<name>.urlThe internal connection URL, password included, for apps: DATABASE_URL=${{secret.db.<name>.url}}.The database's own engine reads them only on first start, when the data directory is empty. So a database deleted and created again with the same name reuses its passwords (they are kept with the volume), and changing a password means changing it in the database too.
No published port by default.
--publish [IP:]PORT(publishindatabase_create) publishes it on the host (default address127.0.0.1), load-balanced like any app port;isb db showthen lists the external URL.
isb db show NAME (database_get) prints the connection details with the
password as a reference; --show-password (reveal: true) reads the secret
and prints the value. Members of the org may do that, as they may read any
org secret (Users, roles and superadmins); the
read is in the audit log.
Everything else is the app tools: isb app deploy pg, isb app update pg
(env such as POSTGRES_INITDB_ARGS, resources, the version), isb app rollback, isb db rm (app_delete). The engine, the database name and the
user are fixed: they live in the data volume. A new major version over old
data is the engine's business (Postgres refuses to start; restore a backup
into a new database instead).
Known issue: MongoDB 8.0 and 8.2 refuse to start on Linux 6.19 and newer (SERVER-121912).
Backups#
Backups go to an S3-compatible destination: AWS S3, Cloudflare R2, Backblaze B2, MinIO, RustFS, Garage. The daemon runs the engine's own dump inside the database's instance, compresses the stream and uploads it; the database's org needs no network path to the bucket, and the dump is never written to the host's disk.
$ export ISB_S3_SECRET_KEY=... # or type it on stdin
$ isb backup dest create offsite --endpoint https://s3.eu-central-1.amazonaws.com \
--region eu-central-1 --bucket acme-backups --prefix isb --access-key AKIA...
created destination offsite
test: ok (212 ms)
$ isb backup create pg-nightly --database pg --destination offsite --schedule '0 3 * * *' --keep 14
created backup pg-nightly; next run 2026-10-04T03:00:00Z
$ isb backup run pg-nightly # now, following the log
$ isb backup files pg-nightly
TAKEN SIZE KEY
2026-10-03T09:09:12Z 47820226 isb/acme/pg-nightly/pg-20261003T090912Z.postgres.gz
isb backup dest create NAME --endpoint URL --bucket B [--region R] [--prefix P] [--path-style]
(--access-key ID | --access-key-secret S --secret-key-secret S) [--create-bucket] [--no-test]
the secret key comes from $ISB_S3_SECRET_KEY or stdin
isb backup dest ls [--json] | rm NAME | test NAME
isb backup create NAME (--database DB | --volume V) --destination D --schedule CRON [--keep N]
[--compression gzip|zstd|none] [--timezone +HH:MM]
isb backup update NAME [--schedule CRON] [--keep N] [--destination D] [--enable|--disable]
isb backup ls [--json] | files NAME [--json] | rm NAME
isb backup run NAME [-d] back up now; follows the run's log
isb backup runs [NAME] [--restores] [--json] | logs [NAME] [RUN] [--restore] [-f]
Destinations (backup_destination_*): endpoint, region (default
us-east-1), bucket, key prefix, and path_style (endpoint/bucket/key:
MinIO and most self-hosted stores; off means bucket.endpoint/key, as AWS
and R2 want). The key pair is stored as the org secrets
backup.<name>.access-key and backup.<name>.secret-key (or name existing
secrets with --access-key-secret/--secret-key-secret). Creating one
writes, reads back and deletes a small object unless --no-test;
--create-bucket creates the bucket first. An endpoint on the daemon's own
host (loopback, link-local) is refused unless the local CLI or a platform
admin creates it, so an org cannot point the daemon at host-local services.
A backup can also take a named volume instead of a database
(--volume): its snapshot is exported and streamed the same way, and it
restores staged beside the volume (Volumes).
Backups (backup_create): a database, a destination, a cron schedule
(Scheduled jobs), keep (default 7), compression
(gzip, the default; zstd; none), timezone, enabled. Each run:
dumps inside the database's running instance, as root, with the instance's own credentials (never argv from the daemon), for at most 12 hours:
Engine Dump Restore postgres pg_dump --format=custom -Z0pg_restore --clean --if-exists --no-owner --no-privilegesmysql mysqldump --single-transaction --routines --triggers --eventsmysqlmariadb mariadb-dump --single-transaction --routines --triggers --eventsmariadbmongodb mongodump --archive --db <database>mongorestore --archive --drop, renamed to the target's databaseredis SAVE, then the RDB filethe RDB file swapped in, the server restarted compresses it and streams it to
<prefix>/<org>/<backup>/<db>-<YYYYMMDDTHHMMSSZ>.<engine>.gz|.zst: one 16 MiB part in memory at a time, a singlePUTwhen it fits in one, a multipart upload otherwise (aborted on any failure). Requests are signed with AWS Signature Version 4.checks the object's size with
HEAD,deletes the oldest of the backup's objects beyond
keep(only objects named as above; anything else under the prefix is left alone),records the run (status, trigger, duration, key, sizes, log) and emits
backup.succeededorbackup.failedon the event feed (Notifications can tell you).
A backup that comes due while the previous one is still running is skipped
(recorded as skipped). isb backup runs NAME and isb backup logs NAME [RUN] show the history. Deleting a backup (backup_delete) keeps its
objects in the bucket.
Restores#
$ isb backup restore pg-nightly --new pg-copy # a new database beside pg
$ isb backup restore pg-nightly --key isb/acme/pg-nightly/pg-20261003T090912Z.postgres.gz --into pg
restore the backup into pg, replacing its data? [y/N]
isb backup restore [BACKUP] [--destination D --key K] (--into DB [-y] | --new NAME [--project P]
[--environment E]) [-d] --into asks before replacing the database's data
backup_restore takes a backup (its newest object, or key), or a
destination and key (for a backup deleted since), and restores into:
target: an existing database of the same engine (MySQL and MariaDB dumps restore into each other). Its data is replaced, so the tool needsconfirm: true, and the CLI asks (or--yes).new: {name, project?, environment?, version?}: a database created for it, by default in the backed-up database's project and environment with its version, with fresh credentials of its own. It is deployed and healthy before the restore starts.
The object streams from the bucket through the daemon into the restore
command's stdin. Restores are runs too (isb backup runs --restores, isb backup logs --restore RUN) and emit restore.succeeded or
restore.failed. A volume backup is refused here: it restores staged
(Volumes).
Tools#
| Tool | Does |
|---|---|
database_create |
A database app (engine, version, database, user, publish, env, resources), deployed unless deploy: false. |
database_list, database_get |
Databases with connection details; reveal adds the password. |
backup_destination_create, _list, _delete, _test |
Destinations. Loopback endpoints: local callers and platform admins only. |
backup_create, backup_update, backup_delete |
Backup schedules (database, or volume: Volumes). |
backup_list |
Schedules with last and next run; with name, the backup's objects. |
backup_run, backup_runs, backup_run_log |
Back up now (wait), the history, a run's log (restore: true for restores). |
backup_restore |
Restore into target (confirm) or new. |
All act in the org they name, for any member of it (volume backups: admins and owners, see Volumes).
On disk#
<org root>/backups/destinations/<name>.json
<org root>/backups/schedules/<name>/backup.json, runs/<id>.json, <id>.log
<org root>/backups/restores/runs/<id>.json, <id>.log
<org root> is the state directory for the default org and
<state>/orgs/<org>/ for the others. Files are 0600.