Bootstrap a fresh droplet
End-to-end walkthrough: take a blank Ubuntu 24.04 droplet and bring it to ufw + docker + traefik in one stratum apply. The config and the Traefik file it ships are both written below; substitute your host's address where indicated.
What you get
After apply:
ufwanddocker.ioinstalled via apt.- ufw rules allowing
22/tcp,80/tcp,443/tcp. dockerandufwsystemd units enabled and started.- ufw ruleset actually activated (
ufw --force enableviassh_exec). - A
stratum-edgeDocker network for Traefik to attach to. /etc/traefik/traefik.ymlwritten from a local file.- A
traefik:v2.11container on:80and:443, attached tostratum-edge, with/var/run/docker.sockmounted.
11 resources, all created in one apply.
Prerequisites
- A fresh Ubuntu 24.04 droplet with a public IP. Any cloud provider works; the tutorial uses DigitalOcean. Note the IP.
- SSH key access as
root. Most cloud providers let you inject an SSH key at provision time. Stratum usesBatchMode=yes, so the key must already be in your local ssh-agent or referenced in~/.ssh/config. No password prompts — they'll hang the apply. - stratum built. From the repo root:
cargo build --release.
Step 1: verify SSH
ssh root@<ip> echo ok
Expect a single ok and exit 0. If you get a password prompt or a Permission denied, fix that before continuing — stratum won't be able to authenticate either.
Step 2: write the config
Create a bootstrap.strat with the host's address:
host "primary" {
addr = "root@<ip>"
}
Replace <ip> with the droplet's public IP. The rest of the file references host.primary.addr — you don't have to touch anything else.
The bootstrap pulls a Traefik config from files/traefik.yml, resolved relative to the .strat file itself. Place your Traefik static config at files/traefik.yml next to bootstrap.strat.
Step 3: plan
./target/release/stratum plan -c bootstrap.strat
Expected output:
stratum plan
============
+ docker_container.traefik
+ docker_network.edge
+ ssh_exec.ufw-activate
+ system_file.traefik-config
+ system_package.docker
+ system_package.ufw
+ system_service.docker
+ system_service.ufw
+ system_ufw_rule.allow-http
+ system_ufw_rule.allow-https
+ system_ufw_rule.allow-ssh
11 create, 0 update, 0 delete, 0 no-op
(The exact alphabetical order comes from BTreeMap iteration of <kind>.<name> keys.) Read-only — nothing is touched on the host.
Step 4: apply
./target/release/stratum apply -y -c bootstrap.strat
The order in which resources execute is the same as the plan output (a depends-on graph is not implemented). The bootstrap config is hand-ordered to avoid the obvious foot-guns:
ufwanddocker.iopackages install (apt-get updateruns once).- ufw rules are added — before ufw is activated.
dockerandufwsystemd units start.ssh_exec "ufw-activate"runsufw --force enable. The ruleset is now enforcing.- The
stratum-edgeDocker network is created. traefik.ymlis dropped at/etc/traefik/traefik.yml.- The Traefik container starts.
After the work runs, you'll see:
State saved to .stratum/state.json
post-apply drift: 4 unreadable — run 'stratum plan --refresh' to see details
4 unreadable is expected: the three system_ufw_rule resources and one ssh_exec resource always return Observed::Unknown from read (see providers/system and providers/ssh).
Step 5: verify
# Traefik should respond — even if just with a 404 (no routes configured yet).
curl -k -I https://<ip>
# HTTP/2 404 ...
# Container should be running.
ssh root@<ip> docker ps
# CONTAINER ID IMAGE ... PORTS NAMES
# abc123def traefik:v2.11 ... 0.0.0.0:80->80/tcp, ...:443->443 traefik
# UFW should be active and have the three rules.
ssh root@<ip> ufw status
# Status: active
# To Action From
# 22/tcp ALLOW Anywhere
# 80/tcp ALLOW Anywhere
# 443/tcp ALLOW Anywhere
Step 6: re-plan with drift detection
./target/release/stratum plan --refresh -c bootstrap.strat
Every step prints with (no-op) — config matches state. The footer:
0 create, 0 update, 0 delete, 11 no-op
drift: 4 unreadable
The same 4 unreadable (three ufw rules + one ssh_exec) — nothing surprising. If any value on the host drifts from state (e.g. someone manually docker stop traefik), --refresh will surface it:
docker_container.traefik
! DRIFT: resource missing on host (state says exists)
0 create, 0 update, 0 delete, 11 no-op
drift: 1 missing, 4 unreadable
Tear it down
Comment out (or delete) the resource blocks, leaving the host block. The resulting plan is all Delete steps, which trips the destruction guard — pass --allow-destroy to acknowledge:
./target/release/stratum apply -y --allow-destroy -c bootstrap.strat
build_plan emits deletes in reverse alphabetical order of <kind>.<name> (see Delete ordering). For the bootstrap config that conveniently gives you a reasonable teardown order — the Traefik container goes before the docker service, ufw rules go before the ufw service. It is not guaranteed to be safe for arbitrary configs; if you have inter-resource ordering needs, remove resources from the config in stages.
What's next
- Layer your own containers on top of Traefik by adding more
docker_containerresources, or follow the Serve a static site behind Traefik tutorial to add a second app sharing the same state file. - For multiple independent slices on the same host, see Multi-namespace deployments — each slice plans and applies on its own with cross-slice port and name collisions caught at plan time.
- Read providers/system for the full attribute schema if you want to extend the config.