Postgres
Run a real PostgreSQL server, with its roles, schemas, grants and extensions declared in doze.hcl.
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.
A database for your app
Section titled “A database for your app”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:
doze url db# postgres://postgres@db.shop.doze:5432/db?sslmode=disablepsql "$(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).
One server, a role per service
Section titled “One server, a role per service”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.
Grants
Section titled “Grants”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"] }}Extensions
Section titled “Extensions”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.
Two servers side by side
Section titled “Two servers side by side”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.
Migrations and seed data
Section titled “Migrations and seed data”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:
doze run --env -- bin/rails db:seedStart again from nothing
Section titled “Start again from nothing”doze reset dbdoze reset deletes the server’s data and asks first. The next connection
creates it again from doze.hcl.
In the app
Section titled “In the app”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.
fsyncis off andshared_buffersis small, which is fast and fine for data you can recreate. Turnfsync = trueon 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 = 18pins the newest 18 build indoze.lock;version = "18.6"asks for that build. The table below lists them.
Reference
Section titled “Reference”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.
Every field at once
Section titled “Every field at once”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" }}Versions you can declare
Section titled “Versions you can declare”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
Fields
Section titled “Fields”| 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).
Nested blocks
Section titled “Nested blocks”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. |
Module releases
Section titled “Module releases”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 ↗