Kurrent Deployment Guide
Covered roles: controller server + storage node — co-located on one machine for self-testing, or split across servers and scaled out (one Agent per storage server). Steps below follow the two roles; for co-located self-testing, run them in order on the same machine.
This guide follows Kurrent's declarative-configuration model: the control-plane declaration control_plane/kurrent.yaml is generated as a template by kurrent config print init-defaults, edited, then validated and converged by kurrent init (kubeadm-init-aligned); the storage-node declaration storager/kurrent.yaml is generated by kurrent config print node-defaults, edited, then applied by kurrent join (kubeadm-join-aligned).
Topology and Port Planning
┌─────────────────────┐ ┌─────────────────────┐
│ Controller server │ │ Storage node │
│ kurrent-dnsmasq │ LAN │ storager-agent │
│ (DHCP/TFTP 67/69) │◄───────►│ (HTTPS 4840) │
│ kurrent-control- │ │ kurrent-nvmet-host │
│ plane (4839) │ │ (NVMe-oF 4420) │
│ kurrent-webui │ └──────────┬──────────┘
│ (4838 / 443) │ │ storage btrfs/xfs
└─────────┬───────────┘ ┌──────────▼──────────┐
│ PXE network │ Diskless clients │
└────────────────────►│ (Devices) │
└─────────────────────┘Example network plan (replace with your own):
| Item | Value | Note |
|---|---|---|
| PXE subnet | 192.168.80.0/24 | The serving subnet the controller binds (DHCP/TFTP/boot traffic) |
| DHCP pool | 192.168.80.50 – .100 | Handed out to diskless clients |
| Gateway / DNS | 192.168.80.2 / 223.5.5.5 | DHCP options 3 / 6 |
| Controller IP | 192.168.1.10 | LAN management address (WebUI / API / storage-node callback) |
| Storage node IP | 192.168.1.20 | How the controller reaches the Agent (4840) |
| Storage hostname | storage-01 | Set it first; used as the default agent-id |
Ports:
| Port | Protocol | Service | Where |
|---|---|---|---|
| 67 / 69 | UDP | DHCP / TFTP (host network) | Controller server |
| 443 | TCP | HTTPS entry (nginx: WebUI + /api/cp reverse proxy to the control-plane API) | Controller server |
| 4838 | TCP | WebUI (HTTP) | Controller server |
| 4839 | TCP | Control-plane API (direct) | Controller server |
| 4840 | TCP | Storage Agent HTTPS (controller → Agent) | Storage node |
| 4420 | TCP | NVMe-oF data plane | Storage node |
Prerequisites
Linux servers (Debian 12 / Ubuntu 22.04+, x86_64 — one controller, one storage node; co-located for self-testing):
# 1. Docker with Compose v2
docker --version && docker compose version
# 2. Storage node: kernel NVMe-oF target support (required for the nvmet backend)
modprobe nvmet nvmet-tcp
mkdir -p /sys/kernel/config && mount -t configfs none /sys/kernel/config # persist it (see below)
# 3. Storage node: btrfs or xfs is recommended for the data disk (both support
# reflink/FICLONE, so golden-image clones are instant; ext4 has no reflink and
# falls back to full copies, clone time growing linearly with image size)
# Mount example: sudo mkfs.btrfs /dev/sdb && sudo mount /dev/sdb /pool1Persist the configfs mount in
/etc/fstab:none /sys/kernel/config configfs defaults 0 0; addnvmetandnvmet-tcpto/etc/modules-load.d/nvmet.conf.
Step 1: Controller Server
1.1 Get the repo and install the kurrent CLI
git clone https://github.com/dutyc/kurrent && cd kurrent
# CLI without a local build: grab the release binary (shipped since v0.3.0; Linux amd64/arm64, Windows)
curl -sL -o cli/kurrent https://github.com/dutyc/kurrent/releases/download/v0.3.0/kurrent-linux-amd64
chmod +x cli/kurrent
sudo install -m 0755 cli/kurrent /usr/local/bin/kurrent # global install (the rest of this guide uses plain `kurrent`)
kurrent version # verifyVersions move with the Releases page (https://github.com/dutyc/kurrent/releases; Windows:
kurrent-windows-amd64.exe). Alternatively build from source instead of downloading (cd cli && go build -o kurrent ., needs Go 1.27+).
1.2 Generate the control-plane declaration
The declaration file is control_plane/kurrent.yaml (kubeadm InitConfiguration equivalent: the yml is the only input — the CLI only validates and starts). Generate the annotated template, then edit it for your environment:
kurrent config print init-defaults > control_plane/kurrent.yaml # generate template
vim control_plane/kurrent.yaml # fill spec.networking (mandatory five keys: NIC/subnet/DHCP pool/gateway/DNS)The five spec.networking keys are deployment facts and must be filled in explicitly (the template ships example values and comments); the remaining blocks (component PKI policy, TOFU cert, boot behavior, …) carry defaults — leave them as-is.
1.3 Initialize and bring up the control plane
One command validates and converges to the running state (kubeadm-init-aligned: declaration-driven config, CLI as tooling; container orchestration is an internal detail):
kurrent init # validate kurrent.yaml → start/restart the control plane → wait /healthz → restart dnsmasq to load the new confOn startup the controller automatically: renders dnsmasq/dnsmasq.conf from spec.networking (yml is authoritative, conf is derived — do not hand-edit it), and generates the TOFU server cert and component PKI. kurrent init is idempotent — re-run it after editing the declaration and the change takes effect.
1.4 Verify
curl http://127.0.0.1:4839/healthz # {"status":"ok"}
head -5 dnsmasq/dnsmasq.conf # interface=ens33 ... (rendered from yml)Open http://<controller-ip>:4838 in a browser (WebUI). For production, set an admin token by exporting KURRENT_CP_TOKEN=<secret> before docker compose up -d (the credential never goes into the declaration file).
Firewall (controller server):
sudo ufw allow 67,69/udp && sudo ufw allow 443,4838,4839/tcp && sudo ufw reload1.5 Issue the storage-node bootstrap credentials
kurrent token create --cp-url https://192.168.1.10--cp-url must be the HTTPS entry of the control plane as reachable from the storage node (port 443 /api/cp reverse proxy — Agents enroll/renew through it; derived from the --server host otherwise). Output resembles (the join command carries the address and is ready to paste):
bootstrap token: a1b2c3.d4e5f6a7b8c9d0e1(expires ...,TTL 内可被多次 enroll 复用)
# Run on the storage node (kubeadm-join-aligned; the command carries the control-plane URL,
# so running it auto-generates/updates the declaration):
kurrent join https://192.168.1.10 --token a1b2c3.d4e5f6a7b8c9d0e1The token is a cluster-wide bootstrap credential (kubeadm token create equivalent): not bound to any node, reusable across multiple enrolls within its TTL, so any storage node can join with the same token; TTL is 7 days (
spec.pki.bootstrapTokenTtlDays) — re-issue when expired. No nvmet-host credential needs issuing: it is derived by the control plane when the agent enrolls withbackend=nvmet.
Step 2: Storage Node
2.1 Prepare
git clone https://github.com/dutyc/kurrent
cd kurrent
mkdir -p storager_img # disk image dir (kurrent join default; custom path in 2.3)
curl -sL -o cli/kurrent https://github.com/dutyc/kurrent/releases/download/v0.3.0/kurrent-linux-amd64 && chmod +x cli/kurrent
sudo install -m 0755 cli/kurrent /usr/local/bin/kurrent # install the CLI as in 1.1 (or copy the binary from the controller)2.2 Declare the node config and join the cluster
The node declaration file is storager/kurrent.yaml (kubeadm JoinConfiguration equivalent — the yml is the declaration). No file preparation on the node: the issued command already carries the control-plane URL — paste and run (kubeadm join <endpoint> equivalent):
cd /path/to/kurrent
kurrent join https://192.168.1.10 --token a1b2c3.d4e5f6a7b8c9d0e1kurrent join does three things (kubeadm-join-aligned; idempotent, safe to re-run):
- Generates/updates
storager/kurrent.yaml(when missing, generated from defaults:metadata.name= hostname, the command address synced intospec.controlPlane.url; when present, read in and merged — non-forbid, hand edits are kept) - Writes the generic bootstrap credential
storager/bootstrap/agent.token(given via--token; otherwise the file must already be in place.nvmet-host.tokenis derived automatically by the agent's enroll — nothing to provide by hand) - Syncs the
.envinterpolation keys (component PKI host paths such asKURRENT_AGENT_PKI_HOST) and starts/restarts the agent container (the backend picks the orchestration: nvmet →storager/nvmeof, stgt/lio →storager/iscsi)
Pre-declaring (optional): to customize business keys such as backend/diskDir/advertiseUrl, first generate the annotated template with kurrent config print node-defaults > storager/kurrent.yaml and edit it, then run join (the declaration is read in and merged; hand edits are kept).
The key step for split deployment: the Agent's advertised URL defaults to https://<cp-host>:4840 (co-located form). When the controller and the storage node are on separate machines, the controller must reach the Agent via the storage node's LAN IP — override it in the declaration (kubelet --node-ip analogy; override lives at the declaration layer):
# Edit storager/kurrent.yaml: spec.agent.advertiseUrl → https://192.168.1.20:4840
# Then re-run: kurrent join <cp-url> --token <token> (converges: restarts the agent so the new declaration takes effect)2.3 Bring up the storage data plane
kurrent join already converged-started the agent (the backend picks the orchestration: nvmet → storager/nvmeof, stgt/lio → storager/iscsi):
docker ps # expect: storager-agent / kurrent-nvmet-host (or the stgt/lio equivalents)Custom disk dir: edit
spec.agent.diskDirinstorager/kurrent.yaml(host path, e.g./pool1/iscsi_img), then re-runkurrent join --token <token>(idempotently syncsKURRENT_DISK_DIRinto.envand restarts the agent to apply it).
Firewall (storage node): sudo ufw allow 4840,4420/tcp && sudo ufw reload
2.4 Verify
docker logs storager-agent 2>&1 | grep pki # pki: client cert ok (cn=agent-storage-01)
# back on the controller server:
kurrent agents list
# ID HEALTH BASE_URL ROLE ENABLED TAGS
# storage-01 ok https://192.168.1.20:4840 disk true auto,nvmet,storageStep 3: Minimal End-to-End Check
Plug a bare-metal client into the PXE network with network boot enabled:
# on the controller server: watch the DHCP/boot flow
docker logs -f kurrent-dnsmasq 2>&1 | grep -E "DHCPACK|pxe|ipxe"Expected chain: the client gets an address via DHCP → pulls the boot firmware over TFTP → iPXE re-request → fetches boot scripts from the controller → the device auto-registers into the pool (visible under WebUI → Devices). Claiming/binding devices, disk creation and Worker delivery are covered by the usage guide (coming soon); this document stops at "environment ready + storage node online".
Troubleshooting
| Symptom | Check |
|---|---|
agents list HEALTH not ok | Can the controller reach spec.agent.advertiseUrl (storage 4840 / firewall)? Storage docker logs storager-agent (cert-renewal failures usually relate to PKI dir permissions) |
| join: token invalid / expired | Token TTL (7 days) expired or never issued — re-run kurrent token create, then re-run kurrent join --token <new-token> (tokens are not node-bound; no node/component distinction) |
| No DHCP answers | Does dnsmasq.conf interface= match the real NIC (ip a)? Firewall 67/69 UDP |
| Agent container restarting | docker logs storager-agent; usually the controller is unreachable (controlPlane.url 443 blocked) or the bootstrap credential file is missing (kurrent join --token <token> rewrites agent.token; delete nvmet-host.token and restart the agent to trigger re-derivation) |
| Controller 503 / enroll failures | Is nginx 443 with the /api/cp reverse proxy healthy? curl -k https://<cp-ip>/api/cp/healthz |
Appendix: Declaration Files at a Glance
control_plane/kurrent.yaml (template generated by kurrent config print init-defaults, edited, then validated/started by kurrent init): spec.networking five keys (interface/subnet/dhcpRange/gateway/dns) plus default blocks pki/serverCert/boot/agentTimeoutSec/dnsmasq.reload — the authoritative controller file; dnsmasq.conf is derived from it.
storager/kurrent.yaml (template generated by kurrent config print node-defaults, edited, then validated/converged by kurrent join): spec.agent (backend/advertiseUrl/diskDir/nqnBase) + spec.controlPlane.url — the authoritative node file. The NQN namespace is owned solely by its nqnBase (the controller discovers it via the capabilities report, so Host NQNs and disk NQNs stay in one domain automatically).
Both are runtime files (not committed); templates come from the matching kurrent.yaml.example or kurrent config print.