Provisioning & the seed file

Starter — your first node

Your first node runs the control plane (the web UI + API). Flash the OS image, then:

  1. 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.

  2. Create a file named rasputin-seed.env at 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_ID names 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 by sh and the key contains spaces.

  3. 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.

VariableApplies toWhat it does
RASPUTIN_NODE_ROLEallcontrolplane or compute. Required — first boot waits for it. (The firewall node runs the separate OpenWrt image, not this one.)
RASPUTIN_SSH_AUTHORIZED_KEYallYour 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_IDallNames 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_SERVERallOptional 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_URLcomputeThe control plane’s NATS URL. The first control plane self-inits against its own embedded NATS and doesn’t need this.
RASPUTIN_CP_JOIN_TOKENcomputeA join token minted by the control plane. Not needed by the first control plane.
RASPUTIN_RELEASE_CHANNELcontrol plane onlystable 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:

VariableApplies toWhat it does
RASPUTIN_BUS_AUTHcontrol plane onlyenforce 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_IDcontrol plane onlyWritten 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_HOSTcompute1 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:

  1. Your DHCP server’s NTP server (option 42), if it advertises one — used automatically.
  2. RASPUTIN_NTP_SERVER from the seed, if you set it.
  3. 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.