Skip to content

Configuration reference

doze reads HCL from doze.hcl (and any sibling *.doze.hcl, merged automatically — see splitting config). The file has a fixed root plus one block per thing it runs. There are four kinds of block:

stack = "shop" # root settings
defaults { idle_timeout = "5m" }
module postgres "app" { version = 16 } # an engine from the doze registry
module valkey "cache" { version = 9 }
app "api" { run = "go run ./cmd/api" } # your code

module <type> "<name>" is an engine from the registry, app "<name>" is your own code, and service and task are commands from outside the registry and commands that run to completion.

Jump to an engine: postgres · valkey · kvrocks · ferret · mariadb · temporal · kafka · aws. Your own code: app. Project-level: modules · TLS.

This is the field-by-field reference. For what each engine is and when to use it, see The engines. Each engine’s page on the registry carries the same reference, generated from the module itself, so it can’t be stale.

FieldTypeDefaultDescription
stackstringthe config folder’s nameNames the stack. It scopes local domains (<name>.<stack>.doze) and must be unique among the stacks running on the machine.
listenstring127.0.0.1:6432Base client address. Each instance gets the next port; override per instance with listen, or use a unix:/path.
homestring$DOZE_HOME or ~/.dozeShared toolchain store + cache (deduplicated across projects).
data_dirstring<home>/projects/<slug>This project’s state (data dirs, sockets, logs).
defaults { idle_timeout }duration5mReap an instance after this long at zero connections.
defaults { log_runs }number5How many runs of each service’s log are kept. A run is one start.
defaults { log_size }size"10MB"The most log kept per service, across its runs.
tls { … }blockoffTerminate client TLS for Postgres — see TLS.
FieldTypeDefaultDescription
certstringautoPath to a PEM certificate. Omit cert and key to auto-generate a self-signed cert.
keystringautoPath to the matching PEM private key.
requiredboolfalseReject plaintext TCP clients (require sslmode=require).
tls {} # auto self-signed cert; sslmode=require works

TLS is terminated at the proxy; the backend speaks plaintext over a local socket.

Every module <type> "<name>" { … } block accepts:

FieldTypeDefaultDescription
versionstring/number—The engine version: a major (16 → newest 16.x, pinned) or exact ("16.14"). Required for every module except aws, which takes none.
listenstringnext port from root listenPer-instance client address ("127.0.0.1:5544" or "unix:/path.sock").

version is the only version you declare. The module (the plugin that provides the engine) is selected automatically — the newest release compatible with your doze and the engine versions you declared — and pinned in doze.lock. Some engine arguments are version-gated: using one below the engine version that introduced it fails at doze lint with the argument and required major named (docs mark these, e.g. engine ≥ 18). See modules for the rare overrides.


Values are HCL expressions, not just literals. You can call functions and—most usefully—reference other blocks by <kind>.<name>.<attribute>:

app "api" {
run = "go run ./cmd/api"
port = 8080
env = {
DATABASE_URL = module.app.url # reference → builds the dependency edge
}
}

The kind is the block’s keyword, never the engine: a module postgres "app" block is module.app, not postgres.app.

ReferenceAttributes
module.<name>url, address, host, port, socket, env_var, name, engine, plus whatever the module adds
app.<name>address, host, port, url (all need the app to declare a port), name
service.<name>address, host, port, data, name
task.<name>none — a task is only named in after

A reference does two things: it resolves to the attribute’s value, and it makes the referencing block depend on the referenced one (doze boots and holds the dependency first — no hand-declared ordering). Referencing a block that isn’t declared is a lint error, and reference cycles are rejected. Names are unique across a stack whatever the kind, so module.db and app.db cannot both exist.

Every module exposes these baseline attributes:

AttributeDescription
name / engineThe instance name and its engine type.
addressClient-facing host:port (or unix:/path).
host / portSplit address (empty/0 for a unix socket).
socketUnix socket path (empty for TCP).
urlThe connection string (e.g. postgres://…).
env_varThe conventional variable name (DATABASE_URL, …).

Functions include the common string/collection/number/encoding helpers (upper, join, format, coalesce, merge, jsonencode, …) plus env("NAME") to read a host environment variable (with an optional default).

variable "pg_version" {
type = number
default = 16
}
locals {
app_db = "app_${var.pg_version}"
}
module postgres "app" {
version = var.pg_version
owner = local.app_db
}

variable "<name>" { … } — a typed input.

FieldTypeDefaultDescription
typetypeanyOptional constraint: string, number, bool, list(string), map(string), …
defaultany—Value when no override is given. Omit to make the variable required.
descriptionstringnoneHuman description.
sensitiveboolfalseHint that the value is secret.

Values resolve by precedence (highest first): --var name=value › DOZE_VAR_<name> env var › a sibling *.auto.doze.vars file (name = value assignments) › the default. A required variable with no value is an error.

locals { … } — named intermediate values (local.<name>); may reference variables, functions, and earlier locals.

Stamp several similar instances from one block. Each stamp becomes its own instance with a flat name — <label>_<key> (for_each) or <label>_<index> (count) — addressable like any other (module.shard_0.url, module.tenant_eu.url).

module postgres "tenant" {
for_each = toset(["eu", "us", "apac"]) # → tenant_eu, tenant_us, tenant_apac
version = 16
}
module valkey "shard" {
count = 3 # → shard_0, shard_1, shard_2
version = 9
maxmemory = "${(count.index + 1) * 64}mb"
}
Meta-argValueIn-bodyNames
for_eacha set of strings, or a mapeach.key, each.value<label>_<key>
counta non-negative numbercount.index (0-based)<label>_<index>

count = 0 (or an empty for_each) produces no instances. A block can’t set both. Keys are sanitized for use in names/paths (orders.fifo → orders_fifo).

References already create dependencies, so you rarely need this. For an ordering that isn’t expressed through a reference, after adds it:

app "worker" {
run = "go run ./cmd/worker"
after = [module.cloud]
}

Each entry is a reference to a whole block. The app waits until a module or another app is ready, and until a task has succeeded.

depends_on is the longer form, for when the condition matters: a map of block name to healthy (the default for every reference and every after entry) or started (wait only until the dependency’s process has spawned).

app "worker" {
run = "go run ./cmd/worker"
depends_on = { "cloud" = "started" }
}

Real PostgreSQL (14–17). On boot, doze creates the database (named after the instance) and converges the declared roles, schemas, grants, and extensions.

module postgres "app" {
version = 16
owner = "app"
role "app" { password = "app" }
grant {
role = "app"
database = "app"
privileges = ["ALL"]
}
extensions = ["uuid-ossp"]
}

Block fields

FieldTypeDefaultDescription
ownerstringpostgresRole that owns the database.
encodingstringserver defaultDatabase encoding, e.g. "UTF8".
localestringserver defaultDatabase locale; shorthand for both lc_collate and lc_ctype.
lc_collatestringlocaleCollation, overriding locale.
lc_ctypestringlocaleCharacter classification, overriding locale.
templatestringserver defaultTemplate database to clone from.
connection_limitnumber-1 (unlimited)Database CONNECTION LIMIT.
is_templateboolfalseMark the database a template.
allow_connectionsbooltrueAllow connections to the database.
tablespacestringdefaultTablespace for the database.
commentstringnoneCOMMENT ON DATABASE.
shared_buffersstring16MBPostgres shared_buffers.
max_connectionsnumber50Postgres max_connections.
fsyncboolfalseWhen off (default), also disables synchronous_commit and full_page_writes — fast, not crash-safe. Set true for durability.
autovacuumboolfalseEnable autovacuum.
settingsmap(string){}Raw postgresql.conf passthrough for any parameter without a typed field (e.g. { work_mem = "8MB" }). Applied after the typed tuning; doze-locked params (listen_addresses, …) always win.
extensionslist(string)[]Shorthand for CREATE EXTENSION IF NOT EXISTS per name.

role "<name>" { … } — a login user (default) or, with login = false, a group role.

FieldTypeDefaultDescription
passwordstringnoneLogin password.
loginbooltruefalse makes it a group role.
superuserboolfalseGrant SUPERUSER.
createdbboolfalseGrant CREATEDB.
createroleboolfalseGrant CREATEROLE.
replicationboolfalseGrant REPLICATION.
inheritbooltrueInherit privileges of granted roles.
bypassrlsboolfalseGrant BYPASSRLS (skip row-level security).
connection_limitnumber-1 (unlimited)Max concurrent connections.
valid_untilstringnonePassword expiry timestamp.
member_oflist(string)[]Roles this role is a member of.
commentstringnoneCOMMENT ON ROLE.
configmap(string){}Per-role parameters via ALTER ROLE … SET (e.g. { search_path = "app, public" }).

schema "<name>" { … }

FieldTypeDefaultDescription
ownerstringdatabase ownerRole that owns the schema.

grant { … }

FieldTypeDefaultDescription
rolestring—Grantee role. Required.
privilegeslist(string)—e.g. ["ALL"], ["SELECT","INSERT"]. Required.
databasestringnoneGrant at the database level.
schemastringnoneGrant within a schema.
objectsstringnoneWith schema: tables / sequences / functions (covers current + future objects).
with_grant_optionboolfalseAllow the grantee to re-grant.

extension "<name>" { … } — for options beyond the extensions shorthand.

FieldTypeDefaultDescription
versionstringlatest availableSpecific extension version.
schemastringdefaultSchema to install into.
sourcestringnonePath to a source/bundle for an extension the binary doesn’t ship — see Extensions.
cascadeboolfalseAdd CASCADE to also create dependency extensions.
optionalboolfalseWhen true, an unavailable or failed extension is a warning, not a hard error. By default a missing/failed extension fails convergence and taints the instance.

In-memory, Redis-protocol cache.

module valkey "cache" {
version = 9
maxmemory = "256mb"
}
FieldTypeDefaultDescription
versionstring/number—Major or exact. Required.
passwordstringnonerequirepass.
maxmemorystringunlimitedMemory cap, e.g. "256mb".
maxmemory_policystringserver defaultEviction policy, e.g. "allkeys-lru".
appendonlyboolfalseEnable the AOF persistence log.
savestringoffRDB snapshot schedule, e.g. "3600 1 300 100".
settingsmap(string){}Raw valkey.conf passthrough, e.g. { "lazyfree-lazy-eviction" = "yes" }. Applied last so it overrides typed fields.

RocksDB-backed, Redis-protocol durable KV store.

module kvrocks "store" {
version = 2
password = "default-token"
namespace "tenant_a" { token = "tok-a" }
}
FieldTypeDefaultDescription
versionstring/number—Major or exact. Required.
passwordstringnonerequirepass (also the default-namespace token).
workersnumberserver defaultWorker thread pool size.
settingsmap(string){}Raw kvrocks.conf passthrough, e.g. { "rocksdb.block_size" = "16384" }.

namespace "<name>" { … } — a kvrocks namespace with an access token. Requires password.

FieldTypeDefaultDescription
tokenstring—The namespace access token. Required.

A self-contained, MongoDB-compatible engine: doze runs a private PostgreSQL with Microsoft’s DocumentDB extension behind a FerretDB v2 gateway, exposing only the Mongo wire (MONGODB_URI). One declared instance is one composite backend — no separate postgres to wire up. See the DocumentDB recipe.

module ferret "shop" {
version = "2.7"
port = 27017
database "catalog" {
collection "products" { seed = "./seed/products.json" }
}
}
FieldTypeDefaultDescription
versionstring/number—FerretDB v2.x gateway version. Required.
databaseblock—A Mongo database to ensure (repeatable; label = name), with nested collection blocks.
settingsmap(string)—Extra FERRETDB_* gateway settings.

Full argument/block reference: its registry page.


MySQL-compatible relational database (the MariaDB 11.4 LTS series). Runs on macOS and Linux — doze repackages upstream’s x86_64-Linux binary and builds the other platforms from source.

module mariadb "db" {
version = 11.4
user "app" { password = "app" }
grant {
user = "app"
privileges = "ALL"
database = "db"
}
}
FieldTypeDefaultDescription
versionstring—MariaDB server version (e.g. 11.4). Required.
character_setstring—Default charset for the instance database (e.g. utf8mb4).
collationstring—Default collation.
settingsmap(string)—Extra [mysqld] my.cnf entries.

user "<name>" { … } — a MariaDB account (host, password). grant { … } — a privilege grant (user, privileges, database, table).


A durable workflow engine: the Temporal dev server (single pure-Go binary — services, SQLite store, Web UI). Workers long-poll it, which holds it awake.

module temporal "dev" {
version = 1.1
port = 7233
ui_port = 8233
namespace "orders" {}
}
FieldTypeDefaultDescription
versionstring—Temporal CLI version (e.g. 1.1). Required.
portnumber7233Frontend gRPC port.
ui_portnumber8233Web UI port.
headlessboolfalseDisable the Web UI.

namespace "<name>" { … } — a namespace to create (retention, description). restart { … } — supervisor restart policy (policy, backoff, max_retries).


A single-node Kafka-protocol broker, no JVM. version is the advertised protocol profile (1–4), not a broker build. Its web console serves one port above the broker.

module kafka "events" {
version = 4
auto_create_topics = true
retention = "168h"
topic "orders" { partitions = 3 }
topic "payments" { config = { "cleanup.policy" = "compact" } }
}
FieldTypeDefaultDescription
versionnumber—Advertised Kafka protocol profile — 1, 2, 3, or 4. Required.
auto_create_topicsbooltrueCreate unknown topics on first reference.
default_partitionsnumber1Partition count for auto-created topics.
retentiondurationunboundedDelete segments older than this ("168h").
retention_bytesnumber0 (unbounded)Delete old segments past this per-partition size.

topic "<name>" { … }

FieldTypeDefaultDescription
partitionsnumber1Partition count.
configmap(string)—Per-topic config (e.g. "cleanup.policy" = "compact").

References: module.<name>.address is the bootstrap host:port.


The whole local AWS as one instance: S3, SQS, SNS, DynamoDB (with Streams), Lambda, EventBridge, KMS, SSM, and Secrets Manager behind a single endpoint, with a web console at /_console. Takes no version. Resources are nested blocks, converged on boot; their labels are the resource names your SDK uses.

module aws "local" {
port = 4566
bucket "uploads" { versioning = true }
queue "emails" {
dlq = "auto"
max_receives = 5
}
queue "orders.fifo" {
fifo = true
content_dedup = true
}
topic "signups" {
subscribe {
queue = "emails"
raw = true
}
}
table "sessions" {
key = "session_id:S"
ttl = "expires_at"
}
function "resize" {
code = "./functions/resize" # dir with a provided.al2 bootstrap
timeout = 10
}
rule "order_placed" {
pattern = "{\"source\":[\"shop.checkout\"]}"
targets = ["lambda:resize", "queue:emails"]
}
key "app_key" {}
parameter "/demo/flag" { value = "on" }
secret "db_password" { value = "local-only" }
}

Resource blocks (each repeatable; the label is the name):

BlockWhat it declares
bucketAn S3 bucket — versioning, object_lock (implies versioning).
queueAn SQS queue — fifo, content_dedup, dlq ("auto" creates + wires a dead-letter companion), max_receives, timing fields. FIFO names end in .fifo.
topicAn SNS topic with subscribe blocks (queue/lambda/http, raw, filter).
tableA DynamoDB table — key = "pk:S" shorthand (add sort = "at:N"), ttl, gsi/lsi blocks. Streams on.
functionA Lambda running as a real local process — code (local dir with a provided.al2 bootstrap), timeout, env.
ruleAn EventBridge rule — pattern (a literal JSON string — HCL functions aren’t available here), targets as "kind:name" shorthand.
key / parameter / secretKMS key (real local crypto) / SSM parameter (label = full /path) / Secrets Manager secret (never stomped without force).

References: module.<name>.url is the endpoint for every AWS_ENDPOINT_URL_* variable. Resource names inside the block are plain strings to your app.


An app is a long-running command that is your code, run directly on the host — no Docker, no virtualization. Unlike a module (which doze proxies and idle-reaps), an app is a supervised client of those backends: it starts with doze up, is not put to sleep when idle, gates readiness on a probe, and restarts per a policy when it exits.

app "api" {
dir = "../approvals-engine" # working dir, relative to this file
run = "go run main.go -program api" # run via `sh -c` (pipes, &&, expansion OK)
port = 8080 # doze listens here; the app listens on $PORT
env = {
DATABASE_URL = module.app.url # reference → dependency edge + value
}
env_file = ".env.local" # optional; lower precedence than env
after = [task.migrate] # ordering with no value to reference
setup = "go run main.go -program migrate -command up" # before the app starts
on_ready = "./scripts/notify-ready.sh" # once it is ready
stop = "./scripts/drain.sh" # before it is signalled
ready = http("/health/ready") # probed on the port the app listens on
ready_timeout = "90s"
restart {
policy = "on_failure" # no | on_failure | always
backoff = "1s" # exponential, capped at 30s
max_retries = 5
}
}
FieldTypeDefaultNotes
runstring—Command line, run via sh -c. Required.
dirstringthe file’s dirWorking directory, resolved relative to the declaring file.
portnumbernoneThe app’s public port. doze listens on it, not the app: the app is started with $PORT, a private port doze picks at each start, and doze forwards between the two (see Ports). Exposed as app.<name>.{url,address,host,port}. Omit for a worker with no endpoint.
directboolfalseThe app binds port itself, and doze only starts it: no $PORT, no waking on a connection, no connection count. For anything a TCP proxy cannot carry, such as UDP.
sleepduration or "never""never"Put the app to sleep after this long with nothing holding it; its next connection wakes it. Needs a port and not direct, since only a connection through doze can wake it.
watchlist of globsnoneRestart the app when a file matching one of these (relative to dir, ** for any depth) changes while it is awake. The inputs of the tasks it waits for count too: the task runs first if its inputs changed. Files are checked once a second; a burst of writes is one restart.
consoleURLnoneA web console the app serves on a second port, such as http://127.0.0.1:8025. doze fronts that port too, on the app’s own address, and opening it wakes the app.
envmapnoneEnvironment for the app and its steps. Highest precedence; values may reference other blocks.
env_filestringnonePath to a KEY=VALUE file (lower precedence than env).
afterlist of referencesnoneBlocks to wait for, written as references to the whole block: [task.migrate, module.db].
readyprobetcp() when port is settcp() (the port accepts a connection), http("/path") (a 2xx from the app’s own port), log("text") (the app printed it) or exec("cmd") (exits 0). With no port and no ready, readiness = “the process stayed alive briefly”.
ready_timeoutduration"60s"How long the app gets to become ready. A first go run that has to compile needs more.
setupstring or listnoneCommands run before the app starts, each via sh -c in dir. A failure aborts the start.
on_readystring or listnoneCommands run once the app is ready.
stopstring or listnoneCommands run before the app is signalled to stop.
restartblockpolicy = "on_failure"policy (no / on_failure / always), backoff (base, grown exponentially and capped), max_retries (default 5).

An app’s environment is layered as: the daemon’s own environment → env_file → env, plus three variables doze sets for every command:

VariableValue
PORTThe port to listen on. Set only when the app declares a port and is not direct.
HOSTThe address to listen on, 127.0.0.1. Set with PORT.
DATAA folder of the app’s own that survives restarts.

Nothing else is added: connection strings reach an app by reference (DATABASE_URL = module.app.url), not by injection. An env that sets PORT or HOST on an app with a port is a lint error, because the app would listen somewhere doze is not forwarding to.

app "api" {
run = "go run ./cmd/api"
port = 8080
}

port = 8080 is where everyone else finds the app: localhost:8080, or api.<stack>.doze:8080 with names on. doze holds that port from the moment the stack is up, whether the app is awake or asleep. The app itself is started with PORT set to a private port and must listen there:

addr := net.JoinHostPort(os.Getenv("HOST"), os.Getenv("PORT"))

What that buys:

  • A sleeping app wakes on its first request, the way a database does.
  • doze status knows who is connected: connections to an app are counted, and held by works for apps as it does for modules.
  • Two stacks can both use 8080. With names on, each app has an address of its own. doze also gives the app 127.0.0.1:8080 when nothing else has it, because people type localhost from habit; when something does, status says the app is reachable by name only.

It costs one hop on the same machine: about 0.2 ms per new connection. An app that hard-codes its port, or sets PORT itself, will fail to start because doze already holds the port; either read $PORT or set direct = true. doze runs go/bun/node from PATH; a .go-version/.prototools in dir only triggers a warning on mismatch. The command and all its children are reaped as a process group on stop.

Note: HCL single-line blocks hold one argument only — write restart { … } across multiple lines (one argument per line), as above.


A service is a long-running command that comes from outside the doze registry — anything you can start from a shell. It sleeps when nothing is using it and wakes when something connects, or when something that depends on it starts. A task is a command that runs to completion, such as a migration.

service "mailpit" {
run = "mailpit --smtp 127.0.0.1:$PORT"
port = 1025
}
task "migrate" {
run = "go run ./cmd/migrate up"
inputs = ["migrations/*.sql"]
env = { DATABASE_URL = module.db.url }
}
app "api" {
run = "go run ./cmd/api"
port = 8080
after = [task.migrate]
env = { SMTP = "smtp://${service.mailpit.address}" }
}

A service takes run, dir, port, env, env_file, after, ready, ready_timeout, setup, on_ready, stop, restart and sleep as an app does. It listens on $PORT (a private port; doze holds port itself), and $DATA is a folder of its own that survives restarts.

A service with a download (a URL, or a map of mac / mac_intel / linux / linux_arm to a URL) is checked but cannot be run yet: doze up refuses a stack that declares one.

setup, on_ready and stop are lists of shell lines run around run:

RunsIf it fails
setupOnce per data folder: the first time, after doze reset, or when the list changes.The start fails and run does not start.
runEvery wake; the one command that stays running.Restarted with a backoff, then failed.
readyPolled after run starts. Default: the private port accepts a connection.The start fails after ready_timeout.
on_readyEvery wake, after ready. Connections that woke it wait for it.The command keeps running but is not ready, so what depends on it does not start. Fix it and doze up <name> runs this step again, and only it.
stopEvery sleep, instead of the signals.Logged; the signals follow.

With no stop, a command is asked to exit with SIGTERM, then killed after a grace period. run must stay in the foreground: docker run -d returns at once and looks like an exit. Each step is a line in doze logs and an event in the journal, with how long it took.

A task takes run, dir, env, env_file, after, inputs and manual. It runs, in order:

  • when something that lists it in after (or references it) starts, if it has never succeeded, failed last time, or its inputs changed — the content of the files the globs match, not their timestamps;
  • when you run doze task <name>, whatever its state.

A manual = true task runs only the second way. A task that fails stops what is after it, and runs again the next time. doze task with no name lists every task and how it last ran; the result is kept in .doze/state.json.


Every module block is backed by a module — a signed plugin doze fetches from the doze registry on first use. module postgres "db" resolves to the module doze/postgres; doze selects the newest release compatible with your doze and your declared engine versions, and doze.lock freezes the choice. There is one registry and nothing to configure: no mirror, no per-module source, no modules {} block. DOZE_MODULES=off disables fetching (offline, apps only), and DOZE_<TYPE>_PLUGIN runs a module you built yourself.

Module workflow in one breath: discovery and docs live on the registry; updates are explicit — doze modules upgrade --check reports, doze modules upgrade moves the pins (commit the updated doze.lock). A declared engine version the pinned module doesn’t support fails with exactly that upgrade command. See the CLI reference.

Root settings live in doze.hcl; instance blocks may be split across sibling *.doze.hcl files (merged automatically), or pass --config <dir> to merge every *.hcl in a directory. See Files & storage.

A bare major (version = 16) resolves to the newest minor and is pinned in doze.lock; a dotted string (version = "16.14") pins exactly. The lock also pins each engine’s module (release, plugin protocol, supported engine versions, per-platform checksums). Commit doze.lock — every machine then runs byte-identical modules and engine binaries. Run doze binaries available <engine> to see engine versions; doze modules upgrade --check to see waiting module updates. See Managing binaries and Files & storage.

doze env prints these, doze run --env runs a command with them set, and the full set is written to .doze/endpoints.yaml for external tooling. An app block does not receive them automatically: it gets the environment it declares, so pass each one by reference (DATABASE_URL = module.app.url). See Workflows.

VariableFor
DOZE_<NAME>_URLevery instance, always
DATABASE_URLthe single postgres instance (if unambiguous)
REDIS_URLthe single valkey/kvrocks instance
MONGODB_URIthe single ferret instance
AWS_ENDPOINT_URL_S3 / _SQS / _SNSthe AWS services, plus AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION (dummy values)