Docs ›
Provisioning & the seed file
Starter — your first node
Your first node runs the control plane (the web UI + API). Flash the OS image, then:
Plug the flashed card or drive into your computer and mount the volume labeled
RASPUTIN-OS— the small FAT seed partition. Go by the label, not size: the Pi image has several FAT partitions.Create a file named
rasputin-seed.envat its root with three lines:RASPUTIN_NODE_ROLE=controlplane RASPUTIN_NODE_ID=cp-1 RASPUTIN_SSH_AUTHORIZED_KEY="ssh-ed25519 AAAA… you@laptop"(Copy-exact template: rasputin-seed.env.example.)
RASPUTIN_NODE_IDnames the control plane on the fleet — any short lowercase name works (cp-1,home-cp). Use your own SSH public key, and keep the double quotes — the file is read byshand the key contains spaces.Boot the node and open http://rasputin.local. The first-run wizard registers a passkey and lands you on the dashboard.
That’s the whole happy path. The first control plane self-initializes against its own embedded NATS — it needs no join token or NATS URL, just its role, an id, and your key.
The seed file
rasputin-seed.env lives on the RASPUTIN-OS volume and is read once, on first boot, to pick the node’s role and join the fleet. Leave it blank for an un-provisioned image — first boot waits until the role is set. Every entry is KEY=value, one per line.
| Variable | Applies to | What it does |
|---|---|---|
RASPUTIN_NODE_ROLE | all | controlplane or compute. Required — first boot waits for it. (The firewall node runs the separate OpenWrt image, not this one.) |
RASPUTIN_SSH_AUTHORIZED_KEY | all | Your SSH public key for root, double-quoted. The image bakes no key, so this is the only way in over the network — leave it blank and SSH is unusable (the local console still works). One key line. |
RASPUTIN_NODE_ID | all | Names the node on the fleet. Required on the control plane — a control-plane seed without it stops first boot with an error, since the control plane’s identity must be stable. On a compute node it’s optional and defaults to the hardware serial; the Add-Node flow and rasputin-provision assign it and bind the join token to it. |
RASPUTIN_NTP_SERVER | all | Optional NTP server to pin (host or IP; double-quote a space-separated list). Only needed to force a homelab-local time server — see Time sync. |
RASPUTIN_NATS_URL | compute | The control plane’s NATS URL. The first control plane self-inits against its own embedded NATS and doesn’t need this. |
RASPUTIN_CP_JOIN_TOKEN | compute | A join token minted by the control plane. Not needed by the first control plane. |
RASPUTIN_RELEASE_CHANNEL | control plane only | stable or dev — which channel Check-for-Updates tracks. Optional; blank → stable. |
Those are all you need for an ordinary cluster. The seed accepts three more, which
rasputin-provision sets for you when it applies and which you would only write by hand
in the cases named below:
| Variable | Applies to | What it does |
|---|---|---|
RASPUTIN_BUS_AUTH | control plane only | enforce requires every node to present a join token on the message bus. A matched set generated by rasputin-provision ships this on, so a pre-paired cluster comes up enforced with no manual step. Blank leaves it off. |
RASPUTIN_SELF_NODE_ID | control plane only | Written automatically from RASPUTIN_NODE_ID — the control plane needs to recognise itself, so it skips itself during fleet updates and defaults to being its own BMC host. Setting it by hand is not useful. |
RASPUTIN_BMC_HOST | compute | 1 marks this node as driving a BMC bus over its own serial port — only for a rack manager on a BitScope blade rack. It suppresses the login prompt and kernel messages on that port, because there the port is a command channel and stray output is live power traffic. Takes one extra reboot during first boot. Wrong on any other node, and not how you configure a Turing Pi — see BMC. |
That is the complete set. Anything else you may see in /var/lib/rasputin/node.env on a
running node is written by the node itself, not read from the seed.
The SSH key must be double-quoted: the value contains spaces and the file is sourced by sh. First boot appends it to the persistent partition (the rootfs is read-only) and runs once; to rotate or revoke a key later, edit /var/lib/rasputin/dropbear/authorized_keys on the node directly.
Time sync
Every node needs a correct clock. The control plane mints its HTTPS certificate against the current time, so a node whose clock is wrong serves a certificate your browser rejects as expired (or not-yet-valid). Nodes with no battery-backed real-time clock — the Raspberry Pi 5 — rely entirely on NTP for this, so if you ever see an expired-certificate warning on a freshly flashed node, the clock is the first thing to check.
You normally don’t configure anything — a node gets its time automatically, in this order of precedence:
- Your DHCP server’s NTP server (option 42), if it advertises one — used automatically.
RASPUTIN_NTP_SERVERfrom the seed, if you set it.- Built-in public fallback — anycast Cloudflare/Google IPs, used only when neither of the above is known. These are numeric, so they work even on a network with no working DNS.
Set RASPUTIN_NTP_SERVER only to pin a specific time server — for example an isolated LAN whose DHCP doesn’t advertise NTP:
RASPUTIN_NTP_SERVER="ntp.homelab.lan"
Double-quote the value if you list more than one server (space-separated), the same way you quote the SSH key.
Adding more nodes
Additional nodes need a node id and a join token minted by the running control plane — set RASPUTIN_NODE_ID, RASPUTIN_CP_JOIN_TOKEN, and RASPUTIN_NATS_URL in that node’s seed. You don’t hand-write these: the control plane’s Add-Node flow and the
rasputin-provision
matched-set CLI (it ships in the control-plane repo) generate a seed bound to each node id. The firewall is a separate x86 image with its own seed — see rasputin-openwrt-firewall.
Something wrong or missing on this page? Tell us — docs bugs count too.