prorm db up runs a database on your machine in Docker or Podman: it picks andocker-compose.yml and a git-ignored .env of$ prorm db up --dialect postgres --ext vector,postgis
$ prorm db url
postgres://prorm:****@localhost:5432/postgres
$ prorm db status
$ prorm db down # keeps the data; --volumes deletes itprorm db init --docker still scaffolds a bare compose file with a localdb up is the managed path. See db init and the local| Command | Does |
|---|---|
db up [name] [--dialect] [--ext] [--name] [--port] [--version] [--image] | Write the files, start the container, install extensions. --no-start only writes; --dry-run prints the files (secrets masked) and the commands; --print-run also prints a single docker run; --force overwrites a compose file you edited. |
db down [name] [--volumes] | compose down; --volumes deletes the data (asks, or --yes). |
db status [name] | Engine, image, port, container health and the redacted URL, for one or all. |
db url [name] [--show-secret] | The connection URL — password masked unless --show-secret. |
--dir (default docker/local-db) and, where it runs--runtime docker|podman. The name defaults to the dialect fordb up, and to the only local database for the others.docker/
local-db/
postgres/
docker-compose.yml # no secrets in it
.env # 0600, git-ignored
Dockerfile # only when an extension has to be added
initdb/
10-prorm-extensions.sql # PostgreSQL, when extensions were asked for
mysql/ # a second database gets its own directory
docker-compose.yml
.env
.gitignore # gains docker/local-db/*/.envmigrations/ (seeDATABASE_URL,db url --show-secret prints.--no-start writes everything and starts nothing, so you can read what it made$ prorm db up --dialect postgres --ext vector --no-start
✓ Wrote ./docker/local-db/postgres/docker-compose.yml (pgvector/pgvector:0.7.4-pg16).
Credentials: ./docker/local-db/postgres/.env (0600; added docker/local-db/*/.env to .gitignore).
Start it with: prorm db up --name postgres (or: docker compose -f ./docker/local-db/postgres/docker-compose.yml up -d)
$ prorm db status
name dialect image port state url
postgres postgres pgvector/pgvector:0.7.4-pg16 5432 not running postgres://prorm:****@localhost:5432/postgresdb up reuses the .env it wrote, so the credentials, and later the$ prorm db up --name postgres
✓ postgres is up on 127.0.0.1:5432 (docker).
Extensions installed: vector
URL: postgres://prorm:****@localhost:5432/postgres
The full URL: prorm db url --name postgres --show-secret (or DATABASE_URL in ./docker/local-db/postgres/.env)$ export DATABASE_URL=$(prorm db url --show-secret)
$ prorm migration:run
$ prorm db:seedimport { Prorm } from 'ts-prorm-orm';
export const prorm = new Prorm({ dialect: 'postgres', url: process.env.DATABASE_URL });
await prorm.connect();$ prorm db down # container gone, volume kept
$ prorm db down --volumes --yes # container and data gone[!CAUTION] db down --volumesdeletes the named volume that holds the data. It asks
first unless you pass--yes, and there is no undo: the nextdb upstarts
an empty database with the same credentials.
db up --dry-run prints every file it would write, with the secrets masked, andprorm db up --dialect postgres --ext vector,pg_trgm --dry-run:# Generated by `prorm db up` for the local postgres database "postgres".
# Local credentials: git-ignored, never commit. `prorm db url` prints the URL (redacted).
PRORM_DB_DIALECT=postgres
PRORM_DB_IMAGE=pgvector/pgvector:0.7.4-pg16
PRORM_DB_SERVICE=postgres
PRORM_DB_CONTAINER=prorm-postgres
PRORM_DB_PORT=5432
PRORM_DB_USER=prorm
PRORM_DB_PASSWORD=****
PRORM_DB_ROOT_PASSWORD=****
PRORM_DB_NAME=postgres
DATABASE_URL=postgres://prorm:****@localhost:5432/postgres
# Read by the image:
POSTGRES_USER=prorm
POSTGRES_PASSWORD=****
POSTGRES_DB=postgres# ── would run ──
docker compose -f ./docker/local-db/postgres/docker-compose.yml up -d --wait
docker compose -f ./docker/local-db/postgres/docker-compose.yml exec -T postgres psql -v ON_ERROR_STOP=1 -U prorm -d postgres -c 'CREATE EXTENSION IF NOT EXISTS "vector"'
docker compose -f ./docker/local-db/postgres/docker-compose.yml exec -T postgres psql -v ON_ERROR_STOP=1 -U prorm -d postgres -c 'CREATE EXTENSION IF NOT EXISTS "pg_trgm"'$ prorm db up --dialect postgres
$ prorm db url
postgres://prorm:****@localhost:5432/postgrespg_isready; extensions run through psql as the app user.$ prorm db up --dialect mysql
$ prorm db url
mysql://prorm:****@localhost:3306/mysqlmysqladmin ping with the root password read from the container's--ext plugins run through INSTALL PLUGIN, and one that is$ prorm db up --dialect mssql
$ prorm db url
mssql://sa:****@localhost:1433/mssqldb up runssqlcmd … -Q "IF DB_ID(N'mssql') IS NULL CREATE DATABASE [mssql]" inside thesa.$ prorm db up --dialect postgres
$ prorm db up shop --dialect mysql --port 3307
$ prorm db status
$ export DATABASE_URL=$(prorm db url shop --show-secret)db up a month apart get the samedocker-compose.test.yml already chose an image, the same one--image | Dialect | Image | Ships (beyond the engine) |
|---|---|---|---|
postgres (default) | postgres | postgres:16.4-alpine | contrib: pg_trgm, pgcrypto, uuid-ossp, hstore, citext, ltree, btree_gist, pg_stat_statements, postgres_fdw, … |
postgres-debian | postgres | postgres:16.4-bookworm | contrib; the Dockerfile base |
pgvector | postgres | pgvector/pgvector:0.7.4-pg16 | contrib + vector |
postgis | postgres | postgis/postgis:16-3.4 | contrib + postgis, postgis_raster, postgis_topology, postgis_tiger_geocoder, postgis_sfcgal, address_standardizer |
timescaledb | postgres, timescaledb | timescale/timescaledb:2.16.1-pg16 | contrib + timescaledb |
timescaledb-ha | postgres, timescaledb | timescale/timescaledb-ha:pg16.4-ts2.16.1 | contrib + timescaledb, timescaledb_toolkit, vector, vectorscale, PostGIS, pg_cron, pgaudit, pgrouting, hypopg, pg_repack |
mysql | mysql | mysql:8.0.39 | plugins/components: validate_password, component_validate_password, connection_control, semi-sync, group_replication, clone, keyring_file, rewriter, ddl_rewriter, version_tokens |
mariadb | mariadb | mariadb:11.4.3 | plugins: server_audit, simple_password_check, disks, metadata_lock_info, query_response_time, locales, semi-sync |
mssql | mssql | mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04 | — (Developer edition; x86-64, Rosetta on Apple Silicon) |
azure-sql-edge | mssql | mcr.microsoft.com/azure-sql-edge:1.0.7 | — (arm64; retired by Microsoft in 2025, connects to master) |
oracle | oracle | gvenzl/oracle-free:23.5-slim | — (app user in FREEPDB1) |
clickhouse | clickhouse | clickhouse/clickhouse-server:24.8.4.13-alpine | — |
cockroachdb | cockroachdb | cockroachdb/cockroach:v23.2.13 | the no-op shims (pg_trgm, hstore, uuid-ossp, …); runs --insecure |
yugabytedb | yugabytedb | yugabytedb/yugabyte:2.20.1.0-b97 | contrib + pg_hint_plan, pgaudit, orafce, pg_cron |
tidb | tidb | pingcap/tidb:v8.1.1 | — (standalone, passwordless root) |
cratedb | cratedb | crate:5.8.3 | — |
questdb | questdb | questdb/questdb:8.1.1 | — |
risingwave | risingwave | risingwavelabs/risingwave:v2.0.1 | — (single node) |
db2 | db2 | icr.io/db2_community/db2:11.5.9.0 | — (privileged, several GB RAM) |
db up refuses them and points atprorm db init.--version replaces the pinned tag with any tag you give (validated as a tag,17-3.5, pg17) picks the package names.--ext, the first image in the dialect's list that ships every requestedpgvector → pgvector/pgvector, postgis → postgis/postgis,timescaledb,vector → timescaledb-ha). The TimescaleDB images are onlypostgis/postgis, pgvector/pgvector orpostgres:16.4-bookworm) and installs the rest from apt.postgresql.org, whichdb up knows: vector, the PostGIS family, pgrouting,pg_cron, pg_partman, hypopg, pgaudit, pg_repack, pg_hint_plan,pg_squeeze, pg_bigm, orafce, plv8, pgtap, pg_wait_sampling,pglogical, h3, ip4r, prefix, rum, semver, periods. Anything elsepg_cron, pgaudit, pg_stat_statements,pg_squeeze, pg_wait_sampling, pglogical) start PostgreSQL with-c shared_preload_libraries=… (keeping timescaledb on a Timescale image);pg_cron also gets cron.database_name. In the compose file that requestcommand:psql -c 'CREATE EXTENSION IF NOT EXISTS "vector"' over the trusted localINSTALL PLUGIN through the mysql client on MySQL/MariaDB (a plugininitdb/ script with the same statements, so a plain docker compose upIDENTIFIED BY treats specially)..env beside the compose file, mode 0600. The compose fileenv_file:; health checks read $$POSTGRES_PASSWORD from thedocker run line and everydocker/local-db/*/.env (relative to --dir) is added to .gitignore.postgres://prorm:****@localhost:5432/app — throughredactSecrets the rest of the CLI uses. db url --show-secretexport DATABASE_URL=$(prorm db url --show-secret).db up reuses the .env, so the existing data volume still opens.127.0.0.1, not every interface.db up looks for docker with Compose v2 (docker compose), thendocker-compose, then podman compose, then podman-compose;--runtime or $PRORM_CONTAINER_RUNTIME chooses. With neither, the files arebrew install podman podman-compose && podman machine init &&
podman machine start; winget install RedHat.Podman; your package manager). Aup --wait are followed by polling the container's health.db init and the local klockprorm db init makes a database for local work and writes a local klock.klock of kind: "local", mode 0600, holding a$ prorm db init app # SQLite ./app.db, and ./.klock
$ prorm db init shop --docker --dialect postgres # a docker-compose.yml, and ./.klock with the URL
$ prorm db init --url postgres://user:pw@localhost/db --klock ./gate.klock
$ prorm db serve ./.klock --port 7070 # a passwordless gate on 127.0.0.1:7070 (alias db gate)db init writes the URL without it, puts the password in the.env in the klock's directory, and records where it is asurlSecretRef: "env:<NAME>". The name is PRORM_KLOCK_DB_PASSWORD for.klock, or PRORM_KLOCK_DB_PASSWORD_<NAME> for another file namegate.klock → PRORM_KLOCK_DB_PASSWORD_GATE). A new .env is createddb init warns, <file> may not be git-ignored — add it to
.gitignore so credentials aren't committed., when no .gitignore nearbydb serve, readLocalKlock() and LocalKlock*$<NAME> from the environment first, then from that .env. Akeychain:<service>/<account> reference is read from the OS keychain. Whenurl:db init.db init --docker writes still names thedb up's, which reads .env).db serve runs admits a single-use request tokenAuthorization: Klock kl2.…, which LocalKlock.token() makes), or the401 (the token
has already been used). A gate klock may also carry an optional scope{"ops": ["read"], "tables": ["orders"]}) that the gate checks each403 outside it. db init does not write aimport { LocalKlock } from 'ts-prorm-orm';
const klock = LocalKlock.fromFile('./.klock');
const res = await fetch('http://127.0.0.1:7070/query', {
method: 'POST',
headers: {
authorization: `Klock ${klock.token()}`,
'content-type': 'application/json',
},
body: JSON.stringify({ sql: 'SELECT count(*) AS n FROM orders' }),
});
const { rows } = (await res.json()) as { rows: Array<{ n: number }> };GET /healthz is open, for a container health check.extensions list/import/export, and new Prorm({ extensions })db init writes, and the db serve gatedocker-compose.test.yml — the integration-test stack these images follow