Namespaces
A namespace "<name>" { ... } block declares a deployable slice of the infra: a set of .strat files that apply together against a dedicated state file. Namespaces live in a top-level manifest (by convention ./stratum.strat) alongside the host, secret, and provider blocks they share.
host "primary" {
addr = "root@192.0.2.10"
}
namespace "infra" {
configs = ["infra.strat"]
}
namespace "app" {
configs = ["app/web.strat", "app/db.strat"]
}
namespace_block ::= "namespace" string "{" attr* "}"
A namespace is selected on the CLI with -n <name>:
stratum -n infra apply -y
stratum -n app apply -y
Both invocations share the host "primary" declared in the manifest; each writes its own state file (.stratum/infra.json, .stratum/app.json).
Body attributes
| attr | required | type | default | description |
|---|---|---|---|---|
configs | yes | list of string | — | .strat files this namespace owns. Paths resolve against the manifest's directory. The manifest itself is always loaded first; configs entries are loaded after, in order. |
state | no | string | .stratum/<name>.json | Explicit state file path. Relative paths resolve against the manifest's directory. Overridden by a CLI -s flag. |
Names must be unique within the manifest. Duplicates error with duplicate namespace. References (host.x.y, secret.z.value) are not allowed inside a namespace body — only string literals.
What goes in the manifest, what goes in a namespace config
The manifest is the shared scope. The per-namespace configs are the scoped scope.
| block | manifest | namespace config |
|---|---|---|
host | yes (shared) | yes (scoped to ns) |
secret | yes (shared) | yes (scoped to ns) |
provider | yes (shared) | yes (scoped to ns) |
resource | rejected | yes |
namespace | yes | rejected |
A resource block in the manifest is loaded into every namespace (because the manifest is always the first file in the merged set), but it is not scoped to any one of them — it would appear in every namespace's plan, and the cross-namespace validator would flag every container as colliding with itself. Put resources in the per-namespace files only.
A namespace block in a non-manifest file parses fine but is invisible to CLI resolution — -n NAME only inspects whichever file is passed as --manifest. Treat namespace blocks as manifest-only.
State layout
Each namespace gets its own state file:
.stratum/
infra.json # everything declared under namespace "infra"
app.json # everything declared under namespace "app"
_shared.json # implicit per-host _stratum_* resources
The shared file holds the implicit per-host tuning resources (_stratum_swap_*, _stratum_sshd_oom_*, _stratum_sshd_reload_*). They live in a single file because every namespace that targets a given host wants the same swap and the same sshd drop-in — splitting them per-namespace would have the first apply create them, the second apply see them as missing from state, and recreate them. See Architecture: split state for the merge rules.
What references can cross namespace boundaries
| reference shape | works across namespaces? |
|---|---|
host.<name>.<field> | yes (manifest-scoped) |
secret.<name>.value | yes (manifest-scoped) |
depends_on to a sibling ns | no |
depends_on on a docker_container must point at a <kind>.<name> declared in the same namespace. The planner only sees one namespace's resources at a time, so a depends_on edge to a sibling namespace's resource errors as an undeclared target.
If a producer-consumer relationship crosses what becomes a namespace boundary, the workaround is to duplicate the trigger resource on the consumer side. A typical case: a ssh_exec runs docker build to produce an image (producer), and a docker_container in another namespace consumes that image. Move (or duplicate) the build ssh_exec into the consumer's namespace so the depends_on edge is local. See Cross-namespace conflicts in the tutorial for an example.
Cross-namespace conflict checks
When -n NAME is set, plan and apply re-load every sibling namespace's configs (with unresolved secrets tolerated) and check the current namespace's docker_container resources for collisions against them. Two cases are caught:
- Port collision — two namespaces declare a
docker_containerbinding the same(host, host_port). Random-port and bare-port forms are skipped. - Container name collision — two namespaces declare a
docker_containerwith the same(host, name).
Both errors name the offending resource and the sibling that already claims the port or name. See Cross-namespace conflicts for the error shape.
Errors
| condition | error |
|---|---|
Label count other than one (namespace "a" "b" { ... }) | BadNamespaceLabels |
Missing configs = [...] | BadNamespaceBody |
configs entry is not a string | BadNamespaceBody |
| Reference inside the body | BadNamespaceBody (no refs allowed) |
| Duplicate namespace name | DuplicateNamespace (names both source paths) |
-n NAME requested but no manifest at ./stratum.strat (and no --manifest) | CLI: requires a manifest, but ./stratum.strat does not exist |
-n NAME requested but the manifest declares no such namespace | CLI: namespace <name> not declared in <manifest> (known: ...) |
-n and -c passed together | CLI: mutually exclusive |
Bundle mode is unchanged
Without -n, stratum operates in bundle mode — the historical workflow. -c X -c Y -s state.json keeps working exactly as before, the cross-namespace validator is skipped, and state is a single file. See Multi-file configs. Namespaces are opt-in; you don't have to migrate.
Tutorial
See Multi-namespace deployments for the end-to-end walkthrough: writing a manifest, splitting an existing bundle, triggering a port collision and resolving it.