Skip to content
doze
0.2

These are the docs for doze 0.2.

What changed in each release →

Why HCL

Why doze.hcl is HCL and not YAML or JSON, argued from what doze does with it.

On this page

You’ll know why doze.hcl is written in HCL, the language Terraform made familiar, and what that buys you.

The short version: a doze config has types, references between blocks and expressions. YAML and JSON are data formats, and would need a second language on top to do the same.

Every block is checked against a schema. A module checks its own block, so the schema is always the one the running code uses. An argument it does not know is an error with the file, the line and a suggestion:

error: module postgres "app": doze.hcl:6,3-16: Unsupported argument; An argument named "shared_bufers" is not expected here. Did you mean "shared_buffers"?

In YAML the same typo is silent: the key is carried along and ignored, and you find out when the setting you thought you set never applied. Add YAML’s own traps (no read as false, 3.10 read as 3.1, indentation deciding structure) and its ease of starting becomes a cost later. JSON avoids those but cannot hold a comment, which a file teammates read needs.

Blocks refer to each other as values:

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

That reference is three things at once. It fills in the connection string. It orders the start: api waits until app is ready. And it is checked before anything runs:

Error: reference to undeclared module "ap"
on doze.hcl line 4, in app "api":
4: env = { DATABASE_URL = module.ap.url }
no module named "ap" is declared. Did you mean module.app?

In YAML the same link is a string that happens to match a name, enforced by nothing until something breaks while running. Compose has depends_on because a YAML value cannot say “this comes from that service”; the order is written a second time beside the data.

Real projects want three tenant databases from one list, a port each, or a value that differs between a laptop and CI. HCL has variables, locals, functions, for_each and count:

variable "tenants" {
default = { acme = 5432, globex = 5433, initech = 5434 }
}
module postgres "tenant" {
for_each = var.tenants
version = 18
port = each.value
owner = each.key
}

The YAML answer is a template language on top (Helm’s Go templates, envsubst, anchors stretched past their purpose), and then the config is no longer what is in the file. In HCL, the file is the program.

module postgres "app" {
version = 18
role "app" {
password = "app"
login = true
}
extension "vector" {}
}

Blocks with labels have the shape of the thing: a server holding roles and extensions. It reads well in a diff, takes comments, and anyone on your team who has used Terraform reads it already.

HCL is one more syntax, and less common than YAML outside the Terraform world. That cost is paid once, in the first hour. To keep it small, most stacks use only what is on this page: blocks, arguments and references. doze fmt lays a file out, doze lint checks it, and an editor schema at /schema/doze-hcl.json gives completion. Each module’s page in the registry lists every argument it takes, generated from the module itself.