Skip to content
doze
0.2

These are the docs for doze 0.2.

What changed in each release →

Postgres

Run a real PostgreSQL server, with its roles, schemas, grants and extensions declared in doze.hcl.

On this page

Runs PostgreSQL 14 – 18 · macOS and Linux · registry page ↗

Declare a Postgres server in doze.hcl, with the roles, schemas, grants and extensions your app needs, and connect to it like any other Postgres.

doze runs the upstream PostgreSQL server, not a fork, so every client, ORM and extension behaves as it does in production. On its first start doze creates the database and then converges what you declared: it creates what is new, updates what changed and drops what you removed. Your migrations own the tables and the data.

module postgres "db" {
version = 18
}

That is a whole server. Its database is named after the block (db), and doze url db prints the line a client needs:

Terminal window
doze url db
# postgres://postgres@db.shop.doze:5432/db?sslmode=disable
psql "$(doze url db)"

The address is the block’s name in your stack (shop here), on Postgres’s own port. See Names and addresses for how that works and what you get with names off. Connecting wakes the server; it sleeps again two minutes after the last connection closes (Sleep and wake).

To hand the URL to an app, reference it instead of copying it:

app "api" {
run = "go run ./cmd/api"
port = 8080
env = { DATABASE_URL = module.db.url }
}

The reference also orders the start: api waits until db is ready (Start order).

Larder, the made-up grocery shop the docs use as an example, runs one Postgres for seven services. Each service connects as its own role and owns a schema of the same name, so the default search_path finds its tables and it cannot touch anyone else’s:

module postgres "postgres" {
version = 18
port = 5432
role "catalog" { password = "catalog" }
role "orders" { password = "orders" }
schema "catalog" { owner = "catalog" }
schema "orders" { owner = "orders" }
}
app "catalog" {
run = "go run ./services/catalog"
port = 8002
env = {
DATABASE_URL = "postgres://catalog:catalog@${module.postgres.address}/postgres?sslmode=disable"
}
}

module.postgres.address is the host and port; the URL around it says which role to log in as.

A role logs in unless you say login = false; a role that does not log in is a group to hang grants on. A complete set: an app user, a read-only group, an analyst in that group, and an admin:

module postgres "shop" {
version = 18
owner = "shop"
role "shop" {
password = "shop"
connection_limit = 20
}
role "readonly" { login = false }
role "analyst" {
password = "analyst"
member_of = ["readonly"]
}
role "admin" {
password = "admin"
superuser = true
createdb = true
createrole = true
}
}

owner makes a role the owner of the server’s database. Every role field is in the reference below.

Scope a grant to a database, or to a schema, and with objects to every current and future table, sequence or function in it:

module postgres "shop" {
version = 18
owner = "shop"
role "shop" { password = "shop" }
role "readonly" { login = false }
grant {
role = "shop"
database = "shop"
privileges = ["ALL"]
}
grant {
role = "readonly"
schema = "public"
objects = "tables"
privileges = ["SELECT"]
}
}
module postgres "db" {
version = 18
extensions = ["uuid-ossp", "pg_trgm"]
extension "vector" { version = "0.7.0" }
extension "hstore" { schema = "extensions" }
}

extensions is the short form; an extension block pins a version or a schema. Which extensions ship with each server, and how to build one that does not, is in Postgres extensions.

Each block is its own server, with its own data, address and sleep. Two majors run side by side:

module postgres "db" {
version = 18
}
module postgres "legacy" {
version = 16
port = 5433
}

Moving a server’s data to a newer major is doze migrate: Postgres major upgrades.

doze converges structure: the database, roles, schemas, grants and extensions. Run your migrations as a task, so they run before the app that needs them, and again only when the migration files change:

task "migrate" {
run = "bun x prisma migrate deploy"
inputs = ["prisma/migrations/**/*.sql"]
env = { DATABASE_URL = module.db.url }
}
app "api" {
run = "bun run dev"
port = 8080
after = [task.migrate]
env = { DATABASE_URL = module.db.url }
}

For a one-off command against a running stack, doze run wakes what is declared first; --env puts DATABASE_URL and the other connection variables in the command’s environment:

Terminal window
doze run --env -- bin/rails db:seed
Terminal window
doze reset db

doze reset deletes the server’s data and asks first. The next connection creates it again from doze.hcl.

The Postgres service page has a console tab: what is running against the database now, and which databases there are. Its overview shows the address, what is holding it awake and when it will sleep.

  • Dev settings by default. fsync is off and shared_buffers is small, which is fast and fine for data you can recreate. Turn fsync = true on if you keep anything you would miss.
  • A pool keeps it awake. Sleep counts connections, so a pool holding idle connections keeps the server up. That is usually what you want while you work.
  • Exact versions. version = 18 pins the newest 18 build in doze.lock; version = "18.6" asks for that build. The table below lists them.

A real PostgreSQL server per instance — no Docker. Declare roles, schemas, extensions and grants in HCL and doze converges them: creating what’s new, updating what changed, dropping what you removed. Boots on first connect, reaps when idle.

module postgres "app" {
version = 18
port = 5432
owner = "app"
encoding = "UTF8"
locale = "en_US.UTF-8"
connection_limit = 50
comment = "primary app database"
shared_buffers = "256MB"
max_connections = 100
settings = {
log_min_duration_statement = "200ms"
}
role "app" {
password = "app"
login = true
createdb = true
connection_limit = 20
member_of = ["readers"]
}
role "readers" {
login = false
}
schema "analytics" {
owner = "app"
}
extension "pg_trgm" {}
grant {
role = "app"
privileges = ["ALL"]
schema = "analytics"
objects = "tables"
}
}

The version = you write is the engine’s own version. Declare a series and doze pins its newest published build, or declare an exact build. Either way it is fetched, verified and pinned in doze.lock, so it never moves on its own.

version = pins (today) exact builds
14 14.24.0 25
15 15.19.0 20
16 16.15.0 16
17 17.11.0 12
18 18.6.0 6
Every exact build, per series

14: 14.24.0 · 14.23.0 · 14.22.0 · 14.21.0 · 14.20.0 · 14.19.0 · 14.18.0 · 14.17.0 · 14.16.0 · 14.15.0 · 14.14.0 · 14.13.0 · 14.12.0 · 14.11.0 · 14.10.0 · 14.9.0 · 14.8.0 · 14.7.0 · 14.6.0 · 14.5.0 · 14.4.0 · 14.3.0 · 14.2.0 · 14.1.0 · 14.0.0

15: 15.19.0 · 15.18.0 · 15.17.0 · 15.16.0 · 15.15.0 · 15.14.0 · 15.13.0 · 15.12.0 · 15.11.0 · 15.10.0 · 15.9.0 · 15.8.0 · 15.7.0 · 15.6.0 · 15.5.0 · 15.4.0 · 15.3.0 · 15.2.0 · 15.1.0 · 15.0.0

16: 16.15.0 · 16.14.0 · 16.13.0 · 16.12.0 · 16.11.0 · 16.10.0 · 16.9.0 · 16.8.0 · 16.7.0 · 16.6.0 · 16.5.0 · 16.4.0 · 16.3.0 · 16.2.0 · 16.1.0 · 16.0.0

17: 17.11.0 · 17.10.0 · 17.9.0 · 17.8.0 · 17.7.0 · 17.6.0 · 17.5.0 · 17.4.0 · 17.3.0 · 17.2.0 · 17.1.0 · 17.0.0

18: 18.6.0 · 18.4.0 · 18.3.0 · 18.2.0 · 18.1.0 · 18.0.0

Field Type Default Description
version number — Engine major to run — 14, 15, 16, 17 or 18. Required.
owner string — Owner role for the instance’s default database.
encoding string UTF8 Character-set encoding for the database.
locale string — Locale (LC_COLLATE + LC_CTYPE) for the database.
lc_collate string — Collation order, overriding locale.
lc_ctype string — Character classification, overriding locale.
template string — Template database to create from.
connection_limit number -1 Max concurrent connections to the database.
is_template bool false Mark the database as a template.
allow_connections bool true Whether the database accepts connections.
tablespace string — Tablespace the database lives in.
comment string — COMMENT applied to the database.
shared_buffers string 16MB shared_buffers server setting, e.g. “256MB”.
max_connections number 50 max_connections server setting.
fsync bool false fsync server setting (off for the dev tuning profile).
autovacuum bool true autovacuum server setting.
extensions list(string) — Shorthand list of extensions to CREATE (or use extension blocks).
settings map(string) — Arbitrary postgresql.conf settings, applied verbatim.

Every module block also takes doze’s own fields: port, listen, sleep, after, enabled, routes, count and for_each (doze.hcl reference).

role "<name>" { … } — A login role / user, converged on the server.

Field Type Default Description
password string — Login password.
login bool true Whether the role may log in.
superuser bool false Grant SUPERUSER.
createdb bool false Allow creating databases.
createrole bool false Allow creating other roles.
replication bool false Allow streaming replication.
inherit bool true Inherit privileges of member-of roles.
bypassrls bool false Bypass row-level security.
connection_limit number -1 Per-role connection cap.
valid_until string — Password expiry timestamp.
member_of list(string) — Roles this role is granted membership in.
comment string — COMMENT applied to the role.
config map(string) — Per-role ALTER ROLE … SET settings.

schema "<name>" { … } — A schema within the database.

Field Type Default Description
owner string — Role that owns the schema.

extension "<name>" { … } — A Postgres extension to install (pgvector, postgis, …).

Field Type Default Description
version string — Specific extension version.
schema string — Schema to install the extension into.
source string — Path to a local extension bundle to install from.
optional bool false Skip (don’t fail) if the extension is unavailable.
cascade bool false CREATE EXTENSION … CASCADE for dependencies.

grant { … } — A privilege grant to a role.

Field Type Default Description
role string — The role the privileges go to. Required.
privileges list(string) — Privileges to grant (SELECT, INSERT, ALL, …). Required.
database string — Target database.
schema string — Target schema.
objects string — Object class the grant applies to (tables, sequences, …).
with_grant_option bool false Allow the grantee to re-grant.

The releases of the module that provide this engine. doze picks the newest one that fits your doze and your version =, and pins it in doze.lock; you never write these. Older releases stay available for pinned lockfiles.

Release Engine versions Platforms
0.2.4 14 · 15 · 16 · 17 · 18 3 stable
0.2.3 14 · 15 · 16 · 17 · 18 3
0.2.2 14 · 15 · 16 · 17 · 18 3
0.2.1 14 · 15 · 16 · 17 · 18 3

Signed data: index.yaml ↗ · meta.yaml ↗