Introduction
stratum is a tiny declarative IaC tool, written from scratch in Rust. It's scoped to system bootstrap: install packages, drop config files, manage systemd services, configure the firewall, and run system-tier containers (Traefik, monitoring agents, log shippers). Describe resources in a .strat file, run stratum plan to preview, and stratum apply -y to make it happen. State lives in a JSON file under .stratum/state.json next to your config.
The tool is intentionally small. No plugin system, no remote backend, no cloud SDK. Providers are first-class crates in the workspace; today there are four:
system— packages, services, files, secret files, directories, ufw rules.ssh— shells out to systemsshto run commands and write files.docker— drives the remotedockerCLI over SSH (networks, containers, image builds).git— clones and pins git working trees on a remote host.
What stratum is not
Stratum is not an app-deployment tool. It will not build your application image, manage per-app environments, or rotate deploys. That's the job of the sibling project deployd (different repo). Stratum's role stops at "the host has docker, traefik, a firewall, and the right config files." Deployd takes over from there.
What works today
.stratconfig:host,secret,provider,resourceblocks; nested maps, lists, string/number/bool values; comments (#and//); refs of the formhost.<name>.<field>andsecret.<name>.value;${...}string interpolation for embedding refs inside larger strings.- JSON state at
.stratum/state.json, withcreate/update/deleteactions and a recursive structural diff that ignores state-only provider-computed fields. - CLI:
plan(with--refreshfor live drift detection and--allow-unresolved-secretsfor plan-only review),apply(with-yto execute and--allow-destroyto permit deletes),status(per-host resource snapshot),state list,state show,state merge. Global--env-filefor loading env vars before resolvingsecret { from_env }refs, with auto-load of./.envwhen no flag is passed. - Repeatable
-conplan/apply: multiple.stratfiles merge into one document and evaluate together. Hosts and secrets declared in one file are visible to refs in another. Duplicates across files are hard errors that name both paths. See Multi-file configs. system_package,system_service,system_file,system_secret_file,system_dir,system_ufw_rule(apt + systemd + ufw + file + secret-file + directory-tree management).system_diralso has an empty-dir mode for pre-creating daemon directories without uploading anything.system_secret_filestoressha256only — plaintext never persists in state.ssh_exec(with optionalenvmap for sensitive shell prefixes),ssh_file(run commands, write files).docker_network,docker_container,docker_image. Containers supportdepends_on(planner topo-sorts),healthcheck(post-apply readiness wait),memory/memory_swaplimits, list-formcommandfor argv passthrough, andpull = falsefor locally-built images.docker_imagebuilds images on the host withDOCKER_BUILDKIT=1and tracks the resultingimage_id.git_repo(newgitprovider) clones a remote repo to a fixed path on a host and pins it to a branch, tag, or full SHA. State trackscommit_sha; drift triggersfetch+reset.- Secrets v0:
secret { from_env = ... }orfrom_file = ..., referenced assecret.<name>.valueor${secret.<name>.value}inside strings. Plaintext flows to providers; state stores a{__secret, __secret_sha256}marker for whole-leaf matches and a<secret:NAME:sha256:HEX>inline marker for substring matches inside interpolated strings;diff/diff_observedare marker-aware so no perpetual drift; plan output renders object markers as<secret:name sha:abc123>. See Secrets. - Planner-side validators:
docker_container.portsconflict check across the merged config (fails on(host, ip, host_port)collisions, with0.0.0.0:N/127.0.0.1:Nsymmetry);depends_ontopo sort with cycle and unknown-ref detection. - Post-apply readiness wait: a
docker_containerwith ahealthcheckmap blocks subsequent steps untildocker inspectreportshealthy(60s budget). Otherwise a 500ms cosmetic pause. Provider::readonsystem_*(exceptsystem_ufw_rule),ssh_file, bothdocker_*kinds, andgit_repo— surfaces drift between recorded state and live host reality.- Post-apply self-check: every successful
stratum apply -yre-reads each resource and reportspost-apply drift: clean(or counts of differ/missing/unreadable). content_file = "<relative-path>"onsystem_fileandsource_dir = "<relative-path>"onsystem_dir, resolved against the.stratfile's directory.- Destruction guard:
stratum applyrefuses to run a plan withDeletesteps unless--allow-destroyis passed. Error names the resources, the loaded configs, and the state file path.
What does not work yet
- No drift detection for
system_ufw_ruleorssh_exec— both returnUnknownfromread, so they show up inunreadablecounts (intentional). - No DNS provider.
- No partial / targeted apply — every plan is whole-config.
- No state locking or remote backend.
- No cross-resource refs of the form
<kind>.<name>.<attr>(e.g.docker_image.X.id) — producer-to-consumer wiring still uses the literal tag + adepends_onedge. Planned, not shipped. - The
provider "<name>" { ... }block parses but no shipped provider reads one today.
Quick start
cargo build --release
# write a config
cat > stratum.strat <<'EOF'
host "prod" {
addr = "root@1.2.3.4"
}
resource "system_package" "curl" {
host = host.prod.addr
name = "curl"
}
EOF
# see what would happen
./target/release/stratum plan
# preview again with live drift annotation
./target/release/stratum plan --refresh
# apply for real
./target/release/stratum apply -y
State is written to .stratum/state.json. Inspect it with stratum state list and stratum state show.
Where to go next
- Bootstrap a fresh droplet — end-to-end walkthrough: blank Ubuntu 24.04 → ufw + docker + traefik in one apply.
- Inject a secret into a docker container — the env-var-from-shell-to-container pattern, with rotation.
- The
.stratlanguage — full grammar reference. - String interpolation — embed
${host.X.field}and${secret.Y.value}inside larger strings. - Multi-file configs — one state per droplet, cross-file refs, duplicate handling.
- Secrets — the
secretblock, refs, redaction, the honesty guard. - Providers — what each kind does and the exact attribute schema.
- Architecture — how plan, apply, and drift detection fit together.