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:

  • ufw and docker.io installed via apt.
  • ufw rules allowing 22/tcp, 80/tcp, 443/tcp.
  • docker and ufw systemd units enabled and started.
  • ufw ruleset actually activated (ufw --force enable via ssh_exec).
  • A stratum-edge Docker network for Traefik to attach to.
  • /etc/traefik/traefik.yml written from a local file.
  • A traefik:v2.11 container on :80 and :443, attached to stratum-edge, with /var/run/docker.sock mounted.

11 resources, all created in one apply.

Prerequisites

  1. A fresh Ubuntu 24.04 droplet with a public IP. Any cloud provider works; the tutorial uses DigitalOcean. Note the IP.
  2. SSH key access as root. Most cloud providers let you inject an SSH key at provision time. Stratum uses BatchMode=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.
  3. 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:

  1. ufw and docker.io packages install (apt-get update runs once).
  2. ufw rules are added — before ufw is activated.
  3. docker and ufw systemd units start.
  4. ssh_exec "ufw-activate" runs ufw --force enable. The ruleset is now enforcing.
  5. The stratum-edge Docker network is created.
  6. traefik.yml is dropped at /etc/traefik/traefik.yml.
  7. 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_container resources, 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.