Self-hosting a node
A Tabbify node is a supervisord orchestrator that joins the private mesh, advertises which capabilities it can offer, and spawns a runner per app. The coordinator address is baked into the binary, so bringing a node online is one command — the machine registers itself and the node API starts routing apps to it. Here are the ways to do it.
Apps deploy as Firecracker microVMs built from an OCI image — that's the single runtime. What a node opts into are the capability tags the coordinator routes against: firecracker (run microVMs), docker (build images with a daemon), and builder (act as a build host). A node always carries the base supervisor tag; the capability tags above are added only when the underlying capability is present.
Prerequisites
The mesh transport is userspace WireGuard over a TUN device. Every node needs:
NET_ADMINcapability and/dev/net/tun— to open the TUN device and join the mesh./dev/kvm— only if you want the supervisor to run apps (the Firecrackerfirecrackertag).- A Docker socket — only if you want this node to build images (the
dockertag).
A supervisor never escalates past what you hand it. A node with no /dev/kvm, no Docker daemon, and no builder designation joins the mesh with just the base supervisor tag — it's a mesh peer but advertises no capabilities. Add /dev/kvm and it advertises firecracker; give it a Docker daemon and it advertises docker; designate it a build host (SUPERVISOR_BUILDER) and it advertises builder. Those tags are what the node matches an app against when it places work.
Any systemd Linux: one command
Debian, Ubuntu, Arch, Fedora — x86_64 or aarch64:
curl -fsSL https://get.tabbify.io/supervisor | sudo TABBIFY_JOIN_TOKEN='<your-join-token>' sh
Get the join token from the admin panel: your network → Add a node (it mints
a token scoped to your network with the right tags baked in). The coordinator
validates it — without a token the node is rejected with a 401 and never
joins. If the machine sits behind NAT (home/office), also pass
TABBIFY_MESH_RELAY_ONLY=true so it converges over the relay:
curl -fsSL https://get.tabbify.io/supervisor \
| sudo TABBIFY_JOIN_TOKEN='<your-join-token>' TABBIFY_MESH_RELAY_ONLY=true sh
The installer resolves the latest release, verifies its sha256, lays down the
same versioned layout the self-update engine owns, auto-installs the build-host
tools (git, e2fsprogs, iproute2, iptables, busybox), fetches the
Firecracker guest kernel plus the firecracker/oras helpers (best-effort —
the node joins the mesh even without /dev/kvm, it just won't be able to boot
microVMs until KVM is present), installs the systemd units including the OTA
auto-update timer, and starts the node. With /dev/kvm present it advertises
firecracker and builds inside microVMs by default (docker-less). The last
lines tell you the node's overlay address and control API. Re-running it
upgrades.
NixOS: one rebuild
NixOS gets the declarative path — the module ships as a flake. Pick whichever matches your setup.
Flakes-based system — add the input and import the module in the host's modules:
inputs.tabbify.url = "github:tabbify-io/tabbify-service-supervisor";
imports = [ tabbify.nixosModules.node ];
Classic (non-flakes) configuration.nix — the simplest path, and it needs no experimental features: drop the module file in /etc/nixos/ and import it.
sudo curl -fsSL -o /etc/nixos/tabbify-node.nix \
https://raw.githubusercontent.com/tabbify-io/tabbify-service-supervisor/main/nixos/tabbify-node.nix
then add ./tabbify-node.nix to the existing imports list — don't create a second imports (a duplicate attribute fails evaluation):
imports = [
./hardware-configuration.nix
./tabbify-node.nix
];
Prefer to pull it from GitHub instead of vendoring the file? The
builtins.getFlakeone-liner works too, but only with flakes turned on — addnix.settings.experimental-features = [ "nix-command" "flakes" ];and pass--option experimental-features "nix-command flakes"on the first rebuild:imports = [ (builtins.getFlake "github:tabbify-io/tabbify-service-supervisor").nixosModules.node ];
Open tabbify-node.nix and set nodeName (the fleet display name); on an AMD box switch kvm-intel → kvm-amd. Then:
sudo nixos-rebuild switch
That's it. The module loads the tun and kvm-intel modules (switch to kvm-amd on AMD), installs the host tools the Firecracker runtime shells out to (firecracker, iproute2, e2fsprogs, busybox, oras), and wires four systemd units: tabbify-fetch bootstraps the supervisor binaries + a Firecracker guest kernel over anonymous HTTPS on first boot, tabbify-supervisor runs the node (Type=notify, Restart=on-failure, runs as root for TUN + KVM), tabbify-appsrv serves staged artifacts on :9000, and tabbify-update does a health-gated self-update on demand (systemctl start tabbify-update). No AWS account needed. One thing to supply: the join token — drop it in /etc/tabbify/supervisor.env (TABBIFY_JOIN_TOKEN=<jwt>, chmod 600) before the rebuild (keep it out of the Nix store, which is world-readable), grab it from the admin (your network → Add a node). Set nodeName for the fleet display name, and TABBIFY_MESH_RELAY_ONLY=true if the box is behind NAT. Then the node joins and starts hosting.
Docker: one run
Same node, packaged. The lean image joins the mesh as a bare supervisor; you opt into capabilities by what you expose at launch:
docker run -d --name tbf-sup --restart=on-failure \
--device /dev/net/tun --cap-add NET_ADMIN \
-e TABBIFY_JOIN_TOKEN='<your-join-token>' \
-v tbf-state:/var/lib/tabbify \
tabbify-supervisor
The -e TABBIFY_JOIN_TOKEN is required (admin → your network → Add a node) — without it the coordinator rejects the node (401); add -e TABBIFY_MESH_RELAY_ONLY=true behind NAT. Add --device /dev/kvm (with the :fc image) to advertise the firecracker tag and run microVMs, or mount /var/run/docker.sock plus --network host to advertise docker and build images. The coordinator is baked into the image, so there's nothing to point at; name the node with -e SUPERVISOR_NAME=edge-fra-1. The tbf-state volume holds the mesh identity (mesh-identity.json), so a restart re-claims the same ULA. Drop the volume and the node rejoins as a fresh peer.
Prefer a declarative manifest? tcli node up -f node.toml renders the docker run from a node.toml ([backend] restart, [capabilities] firecracker/docker, [resources] cpus/memory_mb); add --dry-run to print the command without launching.
Verify and operate
Once your node has joined, the public node API lists it in the mesh roster. The coordinator itself is a control-plane service that lives on the overlay and isn't a user endpoint — query the roster through api.tabbify.io instead, with your Tabbify API token:
curl -s https://api.tabbify.io/v1/supervisors \
-H "Authorization: Bearer $TABBIFY_TOKEN" \
| jq '.[] | {ula, display_name, tags}'
Your node appears with its overlay ULA and the capability tags it advertised. The supervisor's own control API binds the peer ULA ([my_ula]:8730), reachable only from inside the mesh — not host loopback. For a quick local sanity check you can run the image in loopback dev mode (no mesh):
docker run --rm -p 8730:8730 tabbify-supervisor --no-mesh --bind 0.0.0.0:8730
curl -s localhost:8730/health # {"status":"ok","firecracker":…,"docker":…,"ula":…}
What's RnD-grade
Be clear-eyed about the current state. Joining is gated by a join token (minted per-network in the admin → Add a node; the coordinator validates it and stamps the node's network + tags from the token claims — see auth). mTLS on the join handshake is the remaining hardening step. In the Docker path supervisord is PID 1, so its death stops the container and every in-container runner; --restart gives cold recovery, not the host-process crash-survival you get from the systemd/NixOS path. Apps run as Firecracker microVMs, which need /dev/kvm on the host — a node without it joins the mesh but can't boot workloads. The systemd and NixOS paths fetch the Firecracker kernel and helpers for you; the base Docker image carries them only in the :fc variant. See the deploy pipeline for the end-to-end app flow.