One Python file. No frameworks. No dependencies you wouldn't trust.
Boot the USB. Open the browser. The installer is already there. One Python file, one HTML file, zero dependencies. No React, no npm, no node_modules, no build step. It just exists — because a web UI shouldn't need a build pipeline. You can read the entire thing in one sitting.
The web UI has two roles. On the live ISO, it is the installer — guiding you through distro selection, disk partitioning, ZFS pool creation, and OS installation. On the installed system, it is the management dashboard — showing pool health, snapshot status, boot environments, and real-time logs. Both roles are served by the same Python process, the same HTML file, the same WebSocket connection.
This was a deliberate decision. The web UI for an infrastructure tool should not have more dependencies than the infrastructure itself. kldload's web UI depends on Python 3 and the websockets module. That's it. No Express. No FastAPI. No Nginx reverse proxy. No Redis session store. No database for user accounts. It's a single process that talks WebSocket to a single HTML page. If it breaks, you read one file to find out why.
Everything the web UI does, you can also do from the terminal. The UI calls the same ksnap, kbe, kdf tools underneath. It's a window into the CLI, not a replacement for it. If the web UI disappeared tomorrow, nothing about the system changes.
The design principle: The web UI is a thin layer over the real system.
Every button maps to a shell command. Every status display reads the same files a human
would cat. The frontend sends JSON over a WebSocket. The backend runs a command
and streams the output back. There is no ORM, no REST API, no middleware, no authentication
framework. The machine is the state. The filesystem is the database. The WebSocket is the wire.
Architecture
The entire web UI is two files. The Python backend is kldload-webui —
approximately 2,300 lines of Python 3 that serves HTTP, handles WebSocket connections,
and dispatches commands. The frontend is index.html — approximately
1,500 lines of vanilla HTML, CSS, and JavaScript with zero external dependencies.
No React. No Vue. No Angular. No Svelte. No TypeScript. No webpack. No npm.
The Python backend does three things: serves static files over HTTP (the
HTML, CSS, and any assets), accepts WebSocket connections from the browser,
and dispatches actions by running shell commands or querying system state.
It uses Python's asyncio event loop with the websockets library
for the WebSocket server and the standard library's http.server for static file
serving. Both run in the same process on port 8080.
Frontend (index.html)
Single HTML file with inline CSS and JavaScript. Opens a WebSocket to the same host it was loaded from. Sends JSON actions, receives JSON responses. Renders everything with vanilla DOM manipulation. No virtual DOM, no templating engine, no build step. The file you edit is the file the browser loads.
Backend (kldload-webui)
Single Python file that runs as a systemd service. Listens on port 8080.
Serves the HTML over HTTP, then upgrades to WebSocket for real-time communication.
Every incoming message has an action field. The backend switches on
that field and runs the corresponding handler — usually a subprocess call
to a CLI tool or a direct system query.
State database (state.db)
SQLite WAL-mode database at /var/lib/kldload/state.db that stores
cluster designs, node inventory, service definitions, templates, VM records, and
audit events. The database is for the web UI's own state — the system of
record is always the filesystem, ZFS, and systemd. The DB can be exported as JSON
and imported on another node for live-to-master handoff.
Answers file
When you click "Install to Disk", the frontend collects every form field into a
JSON payload, sends it over the WebSocket, and the backend writes it as a shell-sourceable
environment file. Then the backend spawns kldload-install-target with that
file as input. The answers file is the contract between the web UI and the installer
— every variable starts with KLDLOAD_.
The architecture is deliberately flat. There are no layers. The browser talks to the Python process. The Python process talks to the shell. The shell talks to the kernel. Three hops from button click to disk write. Compare that to Cockpit (browser → cockpit-ws → D-Bus → systemd → kernel) or Proxmox (browser → pveproxy → pvedaemon → cluster filesystem → QEMU). Every layer is a place for bugs to hide and latency to accumulate.
The WebSocket is bidirectional and persistent. The browser connects once when the page loads. Every action and every response flows over that single connection. During installation, the backend streams log lines in real-time — the browser receives them as individual WebSocket messages and appends them to the log display. No polling. No SSE. No long-polling. Just a WebSocket that stays open until you close the tab.
The complete installation flow
The installer is a single-page wizard. There are no separate screens — every option is visible on one scrollable page. You set your choices, click install, and watch the real-time log. The entire flow, from boot to installed system, takes three to seven minutes depending on profile and hardware.
Here is what happens, step by step, from the moment you boot the live ISO to the moment the installed system is ready.
Step 1 — Live ISO boot
The live environment starts
The ISO boots CentOS Stream 9 as the live environment — regardless of which distro
you plan to install. The live environment includes ZFS kernel modules (precompiled for the
live kernel), all installer tooling, the web UI, and the darksite package mirrors. On desktop
profile ISOs, GNOME starts with autologin as user live (password: live).
On server ISOs, you get a TTY login. In both cases, Firefox or a terminal prompt points you
to http://localhost:8080 where the web UI is already running.
Step 2 — Distribution selection
Pick your target OS
The top of the installer shows a grid of distribution cards. Each card shows the distro name, version, and whether it can be installed offline or requires internet. The currently supported distributions are:
CentOS Stream 9 (offline) — the default. Enterprise-grade RHEL upstream. Most tested, most stable.
Fedora 41 (offline) — bleeding edge. Latest kernel, latest packages.
Debian 13 Trixie (offline) — the universal OS. Stable, predictable, huge package archive.
Rocky Linux 9 (offline) — RHEL binary-compatible. For shops that need RHEL without the subscription.
RHEL 9 (internet required) — Red Hat Enterprise Linux. Requires Red Hat portal credentials or an activation key.
Ubuntu 24.04 (offline) — Canonical's LTS. Popular for desktops and cloud workloads.
Arch Linux (internet required) — rolling release. Always the latest everything. No darksite (by design — rolling releases cannot be frozen).
FreeBSD (internet required) — not Linux, but supported. Native ZFS, jails, bhyve hypervisor.
OpenBSD (experimental) — security-focused BSD. Experimental support.
The offline distributions include complete darksite mirrors baked into the ISO. CentOS, Fedora, Debian, Rocky, and Ubuntu can be installed in a bunker with no network cable. The RPM darksites use file:///root/darksite/ as a local repo. The Debian and Ubuntu darksites are served over HTTP on ports 3142 and 3143 respectively — lightweight Python HTTP servers that the installer points debootstrap at. RHEL pulls from the Red Hat CDN (subscription required). Arch pulls from the Arch mirrors. FreeBSD and OpenBSD pull from their respective package servers.
When you select RHEL, an additional authentication panel appears — you can authenticate with a Red Hat portal username/password or with an activation key and organization ID. The installer uses subscription-manager inside the target chroot to register and enable repositories before pulling packages.
Step 3 — AI assistant (optional)
Local AI — early preview
Below the distro grid is an AI assistant toggle. When enabled, the installer will set up Ollama, download the llama3.1:8b model (trained on kldload documentation), and install Open WebUI for a browser-based chat interface. This is entirely optional and requires 16 GB of RAM and internet access during the initial model download (~6 GB). After first boot, the AI runs completely offline.
The AI model selection is hardware-gated. The web UI queries the
system's RAM and VRAM (via nvidia-smi if an NVIDIA GPU is detected) and
disables model checkboxes that the hardware cannot support. General Chat (8b) is always
available. Code Assistant (14b) requires 16 GB RAM or 10 GB VRAM. Smart Chat (70b) requires
48 GB RAM or 24 GB VRAM.
Step 4 — Profile selection
Choose what kind of system to build
The profile determines what gets installed on top of the base OS. Four profiles are available:
Desktop — GNOME workstation with ZFS on root. Includes all kldload tools (ksnap, kbe, kdf, kstatus, etc.), the web UI dashboard, Sanoid for automated snapshots, WireGuard, eBPF tools, and the full darksite package cache. This is a complete workstation that happens to run on the most advanced filesystem ever built.
Server — headless SSH server with ZFS on root. Same tools as Desktop minus GNOME. Optimized for remote management. Includes the web UI dashboard on port 8080 for browser-based management.
Core — ZFS on root only. No kldload tools, no web UI, no Sanoid, no darksites. A stock distro installation with the single addition of ZFS as the root filesystem. For users who want ZFS and nothing else.
KVM Host — bare-metal hypervisor. Desktop profile plus libvirt, QEMU/KVM, and the kvm-* toolset for managing VMs on ZFS zvols. Instant clones, atomic snapshots, replication — all at the storage layer.
The Core profile is the escape hatch. It proves that kldload is not a vendor lock-in play. Core installs a completely stock distribution — the same packages, the same configs, the same everything you would get from a netinstall ISO — except the root filesystem is ZFS instead of ext4 or XFS. No k-tools. No web UI. No opinion. If you uninstall ZFS from a Core install, you have a vanilla distro. The Core profile exists so that people who just want ZFS on root without any platform can have exactly that.
The storage mode option appears when Core is selected, letting you choose between automatic partitioning (kldload creates the ZFS pool and datasets for you) and manual mode (the installer drops you to a shell where you create your own pool layout, then type exit to continue). Manual mode is for people who want a specific vdev topology, custom dataset hierarchy, or special-purpose pool properties.
Step 5 — Disk and identity
Target disk, hostname, credentials
The disk selector auto-detects all block devices via lsblk and presents them
sorted by priority: NVMe first, then virtio, then SATA/SAS, then USB, and finally the live
boot device (flagged with a warning). Each entry shows the device path, size, type (NVMe, SSD,
HDD, Virtual), and model string. The first NVMe or virtio disk is auto-selected and marked
as recommended. The live boot device is highlighted in amber with a "LIVE BOOT DEVICE —
do not install here" warning.
Below the disk selector: hostname (defaults to
kldload-node), admin username (defaults to admin),
password with real-time strength indicator, password confirmation
with match check, and an optional SSH public key textarea for passwordless
access.
The ZFS encryption dropdown offers two choices: none (standard ZFS, recommended) or AES-256-GCM with a passphrase. When encryption is selected, two additional fields appear for the ZFS passphrase and confirmation. The UI warns that ZFSBootMenu will prompt for this passphrase on every boot and that there is no recovery mechanism if the passphrase is lost.
Step 6 — Localisation
Timezone, keyboard, locale
Three dropdowns: timezone (grouped by Americas, Europe, Asia/Pacific,
Africa/Middle East — defaults to America/Toronto), keyboard layout
(30+ layouts from us to cn — defaults to us), and locale (30+ locales
— defaults to en_US.UTF-8). These are passed as KLDLOAD_TIMEZONE,
KLDLOAD_KEYBOARD, and KLDLOAD_LOCALE to the installer.
Step 7 — Network configuration
DHCP or static
A checkbox toggles between DHCP (the default) and static network configuration. When
static is selected, four fields appear: interface (auto-detected from
the system — shows interface name, link state, and current IP), IP address
with prefix length, gateway, and DNS servers
(comma-separated, defaults to 1.1.1.1,8.8.8.8). The interface dropdown is populated by a
network_info WebSocket call that queries all non-loopback interfaces.
Step 8 — Platform options
Toggle cards for optional features
A 2x2 grid of toggle cards controls optional platform features:
WireGuard — encrypted mesh networking. Enabled by default. Installs WireGuard tools and generates keypairs during bootstrap.
NVIDIA — GPU drivers and CUDA toolkit.
Only clickable when an NVIDIA GPU is detected (the card is greyed out and disabled otherwise,
based on the has_nvidia flag from lspci output).
eBPF — kernel observability tools. Installs bcc-tools, bpftrace, and kldload's eBPF programs for network and syscall tracing.
ZFS Dedup — block-level deduplication. The card warns "5GB+ RAM per TB" because dedup tables must fit in RAM for reasonable performance.
Step 9 — Export format (optional)
Build a golden image
A dropdown below the platform options controls image export. The default is "None —
install to disk only." When an export format is selected, the installer will build the OS
normally, then seal the image for cloning (clear machine-id, remove SSH host keys, enable
cloud-init with multi-datasource config), export the ZFS pool, and convert the disk to the
chosen format with qemu-img convert.
Available formats: qcow2 (KVM, Proxmox, OpenStack), raw (dd-ready), VHD (Azure, Hyper-V), VMDK (VMware ESXi, vSphere), OVA (VMware, VirtualBox portable).
When a format is selected, SCP export fields appear: remote host, user, path, and SSH password or key path. Leave these blank to save the image locally. Fill them in to have the image automatically uploaded to a remote host after conversion.
The golden image workflow is the bridge between kldload and infrastructure-as-code. You boot the ISO, configure exactly the system you want through the web UI, select qcow2 as the export format, and point the SCP target at your Packer build server. The result is a ZFS-on-root, WireGuard-enabled, eBPF-instrumented golden image that cloud-init can repersonalize for each deployment. No Packer provisioner needed — the image is already built. Packer just needs to stamp it with node-specific identity.
The sealing process is thorough: machine-id is truncated (systemd regenerates on first boot), SSH host keys are deleted (sshd-keygen regenerates), DHCP leases are cleared, cloud-init instance state is wiped, bash history and log files from the build are cleaned. The image boots as if it has never been booted before.
Step 10 — Confirm and install
The point of no return
Clicking "Install to Disk" opens a confirmation modal with a red warning: "This will permanently erase the selected disk and install kldload." The modal shows the selected disk device, size, and distro. You must click "Yes, erase and install" to proceed. There is also a "Wipe Disk" button that securely zeros the disk for clean re-testing.
Once confirmed, the frontend collects all form values into a
JSON payload and sends it as an install action over the WebSocket. The backend
writes the answers file, spawns kldload-install-target as a subprocess, and
begins streaming its stdout/stderr back to the browser in real-time. A progress bar pulses
with a purple glow animation. The install form disappears and the log view takes over.
Step 11 — Installation progress
Real-time log streaming
During installation, you see exactly what is happening. The log display shows every line
of output from the installer: partitioning the disk, creating the ZFS pool (rpool),
creating datasets (rpool/ROOT/default, rpool/home, rpool/var),
running dnf --installroot or debootstrap or pacstrap,
installing packages, building ZFS DKMS modules for the target kernel, configuring the bootloader
(ZFSBootMenu), setting up the network, applying profile-specific configuration, and running
firstboot scripts.
If you refresh the browser mid-install, the web UI detects that
an installation is running (via an install_state query) and automatically switches
to the install view, resuming the log stream. Nothing is lost.
If the install fails, the progress bar turns red, the error is
displayed, and a "Back to installer" button appears so you can fix settings and retry. The
backend runs emergency cleanup — unmounting /target and exporting the
ZFS pool — so the disk is in a clean state for the next attempt.
Step 12 — Completion
Remove the USB and reboot
On success, the progress bar fills to 100% and the status reads "Install complete —
system powering off." The installer ejects any optical drives so the machine boots from
disk on next start. If this was an unattended install (autoinstall.env present), the system
auto-reboots after 10 seconds. Otherwise, it waits for you to remove the USB/ISO and reboot
manually with systemctl reboot.
The dashboard
After installation, the web UI transforms into a management dashboard. The installer tab
hides itself (detected via the live_mode flag — if
/proc/cmdline does not contain boot=live, or if
/etc/kldload/node.env exists, you are on an installed system). What remains
is the dashboard, ZFS management, and log viewer.
System overview
Six stat cards across the top: Hostname, Uptime, CPUs, Memory (total and free), Network (primary interface, link state, IP address), and AI Chat (if Ollama is installed — shows ready/starting/offline status with a link to Open WebUI). All stats refresh every 5 seconds over the WebSocket.
ZFS pool status
A panel showing the output of zpool status for all imported pools. Pool
health, vdev topology, read/write/checksum error counts. If a pool is degraded or faulted,
you see it here immediately. Below that, a "Recent Snapshots" panel showing the latest
snapshots with timestamps and sizes.
Firstboot progress
On a freshly installed system, a pulsing purple banner appears at the top of the dashboard
showing firstboot script progress: "Downloading AI models...", "Configuring WireGuard...",
"Building ZFS DKMS...", etc. This status is read from /tmp/kldload-firstboot-status
and disappears when firstboot completes.
The dashboard is not a monitoring tool. It's a status board. It answers one question: "is this machine healthy right now?" Pool degraded? You see it. Snapshot count growing too fast? You see it. Compression ratio dropping on a dataset that should be compressing well? You see it. For actual monitoring — alerting, historical graphs, trend analysis — use Prometheus and Grafana. The dashboard is for the human standing at the console, not for the SRE watching 500 machines.
ZFS management
Pool status
The ZFS view opens with a scrollable panel showing the full output of
zpool status. This is the same output you would see in a terminal —
pool name, state, scan status (scrub/resilver progress), vdev configuration, and error
counts per device. The panel updates on each view switch and can be manually refreshed.
Snapshot management
A table lists all snapshots with columns for name, used space, and actions. The "Take
Snapshot" button creates a recursive snapshot of the entire pool with a timestamped name.
Each snapshot row has a "Rollback" button that reverts the dataset to that snapshot's state.
This calls the same ksnap tooling as the CLI.
Boot environments
A panel showing all boot environments (ZFS datasets under rpool/ROOT/) with
the active one highlighted. Two buttons: "New BE" creates a clone of the current boot
environment (instant, copy-on-write), and "Refresh" reloads the list. Boot environment
switching is done through ZFSBootMenu at the next reboot — the web UI shows you
what exists, the bootloader lets you pick which to boot.
Log streaming
Tail anything from the browser
The Logs view has a dropdown to select which log to stream: Installer
(/var/log/installer/kldload-installer.log), Firstboot
(/var/log/kldload/firstboot.log), or Syslog. Selecting a log
sends a read_log action to the backend, which reads the file and returns its
contents. During active operations, the tail_log action streams new lines in
real-time over the WebSocket.
The backend also supports these log categories via the
LOG_FILES registry: storage, network, bootstrap, snapshots, autoboot, and
audit. Each maps to a specific file path under /var/log/installer/ or
/var/log/kldload/.
During installation, you see exactly what's happening: which packages are downloading, which modules are building, which config files are being written. No guessing. No waiting for it to finish to find out what went wrong. The log streams in real-time because the backend reads the installer's stdout/stderr and pushes each line to the WebSocket as it arrives. If something fails at line 347, you see line 347 the moment it happens, not after a 5-minute timeout.
WebSocket API reference
Everything the web UI does is available as a JSON WebSocket API. The protocol is simple:
connect to ws://host:8080, send a JSON object with an action
field, and receive a JSON response with a type field. Every button in the
browser sends a WebSocket message. Every status update comes back as a WebSocket event.
The HTML is a client. Your automation script can be another client.
Connection lifecycle
The browser connects on page load. On onopen, it immediately sends
system_info, list_disks, zfs_list,
snapshot_list, and install_state to populate the UI. If the
connection drops (onclose), the status indicator turns red and the client
retries every 3 seconds. The dashboard auto-refreshes system_info every 5
seconds while the dashboard tab is active.
System actions
| Action | Response type | Description |
|---|---|---|
ping | pong | Health check. Returns immediately. |
system_info | system_info | Hostname, OS, kernel, CPUs, RAM (total/free), uptime, NVIDIA GPU detection, VRAM, AI status, live mode flag, edition, firstboot progress. |
network_info | network_info | List of network interfaces with name, link state, and IP addresses. |
list_disks | disk_list | All block devices with path, size, model, type (NVMe/SSD/HDD/Virtual), removable flag, transport, priority ranking, and warning labels. |
infra_status | infra_status | Cluster node count, WireGuard peer count, service count, pool health summary. |
Installation actions
| Action | Response type | Description |
|---|---|---|
install | install_log (streamed) | Start installation. Payload includes distro, disk, hostname, password, profile, encryption, network config, platform options, export format. Streams log lines in real-time. |
install_state | install_state | Query whether an installation is currently running. Returns running boolean and rc (exit code) if finished. |
wipe_disk | wipe_result | Securely zero the specified disk. Used for clean re-testing. |
test_rhel_cdn | rhel_cdn_result | Test Red Hat CDN connectivity with provided credentials. |
ZFS actions
| Action | Response type | Description |
|---|---|---|
zfs_list | zfs_list | List pools, datasets, properties. |
zfs_status | zfs_status | Full zpool status output for all pools. |
snapshot_list | snapshot_list | All snapshots with name, used space, creation time. |
snapshot_take | snapshot_result | Create a recursive snapshot with a timestamped name. |
snapshot_rollback | snapshot_result | Roll back a dataset to a specified snapshot. |
be_list | be_list | List boot environments under rpool/ROOT/. |
be_create | be_result | Clone current boot environment into a new one. |
VM and infrastructure actions
| Action | Response type | Description |
|---|---|---|
spawn | spawn_result | Create and start a new KVM virtual machine on ZFS zvol. |
list_vms | vm_list | List all defined VMs with status. |
vm_op | vm_result | Start, stop, destroy, or snapshot a VM. |
vm_clone | vm_result | Instant-clone a VM via ZFS clone. |
vm_replace | vm_result | Replace a VM with a fresh clone of its golden image. |
golden | golden_result | Create or manage golden images. |
golden_status | golden_status | Query golden image build progress. |
WireGuard actions
| Action | Response type | Description |
|---|---|---|
wg_status | wg_status | WireGuard interface status and peer list. |
wg_gen_keypair | wg_keypair | Generate a new WireGuard keypair. |
wg_add_peer | wg_result | Add a peer to a WireGuard interface. |
wg_remove_peer | wg_result | Remove a peer from a WireGuard interface. |
wg_planes_list | wg_planes | List all WireGuard network planes (backplane, management, etc.). |
wg_plane_create | wg_result | Create a new WireGuard network plane. |
wg_plane_delete | wg_result | Delete a WireGuard network plane. |
Cluster and service actions
| Action | Response type | Description |
|---|---|---|
cluster_deploy | cluster_result | Deploy a designed cluster to target nodes. |
cluster_save | cluster_result | Save a cluster design to the state database. |
node_list | node_list | List registered nodes in the inventory. |
node_apply | node_result | Apply configuration to a specific node. |
service_list | service_list | List defined services. |
service_save | service_save | Create or update a service definition. |
service_deploy | service_result | Deploy a service to its target nodes. |
service_delete | service_result | Delete a service definition. |
Database and state actions
| Action | Response type | Description |
|---|---|---|
db_list | db_list | List templates, filtered by type (cluster, node, service). |
db_save | db_save | Save a template to the state database. |
db_load | db_load | Load a template by ID, with full data. |
db_delete | db_delete | Delete a template by ID. |
db_export | db_export | Export entire state database as JSON. |
db_import | db_import | Import a JSON export, with merge or replace semantics. |
db_node_register | db_node_result | Register or update a node in the inventory. |
db_nodes | db_nodes | List all registered nodes. |
db_events | db_events | List audit events (most recent first). |
Log and misc actions
| Action | Response type | Description |
|---|---|---|
tail_log | log_line (streamed) | Stream a named log file in real-time. Specify the log name (installer, storage, bootstrap, etc.). |
read_log | log_content | Read the full contents of a named log file. |
run_audit | audit_result | Run the kldload security audit and return results. |
credentials_load | credentials | Load stored credentials from /run/kldload/credentials.json. |
credentials_save | credentials_result | Save credentials (encrypted at rest). |
credentials_test | credentials_test | Test stored credentials against their service endpoint. |
stamp | stamp_result | Apply a kldload stamp (identity, configuration) to the system. |
poof | poof_result | Destroy infrastructure (cluster teardown). |
This API is how you build on top of kldload.
The WebSocket API isn't an afterthought — the web UI is built on it. Every button in the browser sends a WebSocket message. Every status update comes back as a WebSocket event. The HTML is a client. Your automation script can be another client. Your CI pipeline can be another.
Example: a provisioning system that deploys 50 machines. Each machine boots the kldload ISO. Your orchestrator connects to port 8080 on each one, sends an install command with the config, and streams the logs. 50 parallel installs, all visible, all scriptable, all using the same API the browser uses. No Ansible. No PXE. No kickstart templates. WebSocket in, status out.
Or simpler: a cron job that connects to the API, calls zfs_list, checks compression ratios, and alerts if something changed. Three lines of Python. The API is the back plane for anything you want to build.
Unattended mode
Every installation can run unattended. The answers file is a shell-sourceable environment
file — one KLDLOAD_ variable per line. Place it at
/etc/kldload/autoinstall.env on the live ISO (or inject it via PXE), and
the installer runs headlessly with no human interaction.
Answers file format
KLDLOAD_ environment variables
The answers file is simple: one variable per line, shell-sourceable. The backend writes this file from the web UI form values, or you write it by hand for unattended installs. Every variable has a sensible default. The complete list:
KLDLOAD_DISTRO=centos — target distribution
KLDLOAD_DISK=/dev/vda — target block device
KLDLOAD_PROFILE=server — desktop, server, core, or kvm
KLDLOAD_HOSTNAME=kldload — system hostname
KLDLOAD_USERNAME=admin — admin user account name
KLDLOAD_PASSWORD=... — admin password (hashed or plain)
KLDLOAD_NET_METHOD=dhcp — dhcp or static
KLDLOAD_NET_IFACE=eth0 — network interface (static only)
KLDLOAD_NET_IP=192.168.1.100 — IP address (static only)
KLDLOAD_NET_PREFIX=24 — CIDR prefix (static only)
KLDLOAD_NET_GW=192.168.1.1 — gateway (static only)
KLDLOAD_NET_DNS=1.1.1.1,8.8.8.8 — DNS servers
KLDLOAD_STORAGE_MODE=zfs — storage mode
KLDLOAD_ZFS_TOPOLOGY=single — single, mirror, raidz, stripe
KLDLOAD_ZFS_ENCRYPT=0 — 0 or 1
KLDLOAD_WIREGUARD=0 — 0 or 1
KLDLOAD_NVIDIA_DRIVERS=0 — 0 or 1
KLDLOAD_ENABLE_EBPF=0 — 0 or 1
KLDLOAD_ENABLE_AI=0 — 0 or 1
KLDLOAD_EXPORT_FORMAT=none — none, qcow2, raw, vhd, vmdk, ova
KLDLOAD_EXPORT_SCP_HOST=... — remote host for image upload
KLDLOAD_EXPORT_SCP_USER=root — remote user
KLDLOAD_EXPORT_SCP_PATH=/root/ — remote path
KLDLOAD_INFRA_MODE=standalone — standalone or cluster
KLDLOAD_DEBIAN_RELEASE=trixie — Debian codename
The answers file is the lowest-level interface to kldload. The web UI generates it. The CLI generates it. You can write it with cat and a heredoc. It is a contract: if these variables are set, the installer will produce the described system. This is what makes kldload scriptable. You do not need the web UI. You do not need the CLI wrapper. You need a text file with environment variables and the kldload-install-target script. Everything else is convenience.
For PXE deployments, embed the answers file in the initramfs or serve it over HTTP. The live ISO's init can be configured to fetch the file on boot and launch the installer automatically. Combined with DHCP option 66/67 for PXE booting, this gives you zero-touch provisioning for an entire rack.
Scripting with the WebSocket API
Three lines of Python
You can drive an installation from any language that speaks WebSocket. Here is the minimal Python example:
import asyncio, websockets, json
async def install():
async with websockets.connect("ws://192.168.1.50:8080") as ws:
await ws.send(json.dumps({
"action": "install",
"distro": "centos",
"disk": "/dev/vda",
"hostname": "prod-web-01",
"password": "changeme",
"profile": "server"
}))
async for msg in ws:
data = json.loads(msg)
if data["type"] == "install_log":
print(data["line"], end="")
elif data["type"] == "install_state":
if data.get("rc") == 0:
print("Install complete")
break
asyncio.run(install())
That's it. Connect, send, stream. The same pattern works for any action — replace the action name and payload, parse the response type.
Customizing the UI
The web UI ships in two editions: free and core. Each
edition has its own index.html under
/usr/local/share/kldload-webui/{free,core}/. At build time,
build-iso.sh copies the active edition's directory into
/usr/local/share/kldload-webui/active/, which is what the Python backend
serves. The backend detects the edition from /etc/kldload/edition.
File locations
Backend: /usr/local/bin/kldload-webui — the Python WebSocket server. Runs as a systemd service. Edit this to add new API actions, change behavior, or integrate with external tools.
Frontend (free edition): /usr/local/share/kldload-webui/free/index.html — the full installer and dashboard with all features.
Frontend (core edition): /usr/local/share/kldload-webui/core/index.html — a stripped-down UI with only the install wizard and basic ZFS management. No cluster designer, no service management, no advanced options.
CSS: /usr/local/share/kldload-webui/css/app.css — the stylesheet. Dark theme by default. All colors use CSS custom properties so you can retheme with a handful of variable overrides.
Active edition: /usr/local/share/kldload-webui/active/ — symlinked or copied at build time. This is what the backend actually serves.
Adding a custom field to the installer
To add a new option to the install form:
1. Add the HTML form element to index.html inside the #install-idle div.
2. In the startInstall() JavaScript function, read the value and include it in the JSON payload sent to the WebSocket.
3. In kldload-webui, find the install action handler. The backend writes the answers file from the received JSON — add your new field as a KLDLOAD_ variable.
4. In kldload-install-target or the appropriate library file, read the new variable and act on it during installation.
That's the entire chain: HTML form → JavaScript → WebSocket JSON → Python → answers file → bash installer. Four files, four edits, zero framework magic.
This is why the web UI is one file of HTML and one file of Python. Want to know how the install form works? Read the HTML. Want to know how the backend processes it? Read the Python. Want to add a feature? Edit two files. There is no component tree, no state management library, no routing framework, no build pipeline to understand first. The entire UI is small enough to hold in your head. That is the point.
The Core edition proves this works at scale. It is a different index.html with fewer features — same backend, same API, same installation path. If you wanted to build a completely custom UI for your organization, you could replace index.html with your own design and use the WebSocket API as your interface. The backend does not care what the frontend looks like.
Advanced features
Cluster designer
Design multi-node infrastructure in the browser
The state database supports cluster definitions — collections of nodes with assigned
roles, WireGuard mesh configurations, and service deployments. The cluster_save
and cluster_deploy API actions let you define a cluster topology in JSON and
push it to target nodes. Each node registers itself via db_node_register with
its hostname, management IP, WireGuard public keys, and system facts.
The cluster designer stores everything in the SQLite state
database. Templates can be exported as JSON (db_export) and imported on another
node (db_import) for live-to-master handoff — design the cluster on one
machine, export the state, import it on the actual infrastructure.
Service management
Define, deploy, and track services
Services are defined with a name, description, runtime (systemd, container, etc.), target
nodes, ZFS dataset, container image, replica count, replication targets, and custom config.
Each service gets its own ZFS dataset at rpool/services/{name} by default.
The service_deploy action pushes the service definition to target nodes and
starts it. Service status is tracked in the state database.
Credential management
Stored secrets
The web UI can store credentials at /run/kldload/credentials.json (in tmpfs,
not persisted to disk) for services that need authentication: Red Hat portal, SCP targets,
WireGuard endpoints. The credentials_save, credentials_load, and
credentials_test actions manage these. Credentials stored in /run/
disappear on reboot — this is intentional for security on live ISOs.
VM management
KVM virtual machines on ZFS
On KVM Host profile installations, the API exposes VM lifecycle actions: spawn
creates a new VM on a ZFS zvol, list_vms shows all defined VMs, vm_op
handles start/stop/destroy/snapshot, and vm_clone creates instant ZFS clones of
existing VMs. The vm_replace action destroys a VM and recreates it from its golden
image — immutable infrastructure at the hypervisor level.
What each screen looks like
For users who have not yet booted the ISO, here is a description of each major screen in the web UI. The interface uses a dark theme with a dark blue/charcoal background, green accent color for active elements, and purple for progress indicators.
Sidebar navigation
A narrow sidebar on the left with the kldload logo at the top. Four navigation items: Dashboard (diamond icon), Install to Disk (lightning bolt icon), ZFS & Snapshots (half-filled square icon), and Logs (three-line icon). The sidebar has section headers for System, Install, Storage, and Tools. A small status indicator at the top of the main content area shows a colored dot (green for connected, red for disconnected) and connection text.
Dashboard screen
A release banner at the top with a gradient border (green to purple) showing the current version and changelog highlights. Below that, a grid of six stat cards in a row: hostname in large text, uptime in green, CPU count, memory with free amount, network interface with link state and IP, and AI status (if enabled). Two panels below: ZFS Pools showing a one-line pool summary, and Recent Snapshots showing the latest snapshot names and timestamps.
Install screen
A vertical flow of panels, each with a dark header bar. The distro grid is a 9-column row of cards, each with an emoji icon, distro name, and "offline" or "internet required" label. Selected cards have a green border. Below: the AI assistant toggle card with a large brain emoji, description text, CLI tool reference in a monospace box, hardware requirements, and a toggle switch. Then the profile grid (4 cards: Desktop, Server, Core, KVM Host). Then the disk/identity panel with a dropdown, text inputs, password fields with strength indicators. Then localisation with three dropdowns. Then network with a DHCP checkbox and collapsible static fields. Then platform options as four toggle cards in a 2x2 grid. Finally, the export format dropdown with optional SCP fields. At the bottom, a green "Install to Disk" button and a red "Wipe Disk" button.
Install progress screen
Replaces the install form when installation starts. A header reads "Installing — do not power off." A one-line summary shows the selected distro and profile in monospace. A thin progress bar with a pulsing purple glow animation. A status line in grey text. Below that, a large scrollable log box (340px tall) with monospace text showing real-time installer output — each line appears as the backend streams it. If the install fails, the bar turns red and a "Back to installer" button appears.
ZFS screen
Three panels stacked vertically. Pool Status: a scrollable monospace box showing full
zpool status output. Snapshots: a table with Name, Used, and Actions columns,
plus a purple "Take Snapshot" button in the panel header. Boot Environments: a scrollable
monospace box showing BE names and active markers, with "New BE" and "Refresh" ghost buttons
below.
Logs screen
A single panel with a dropdown selector in the header (Installer, Firstboot, Syslog). Below, a large scrollable monospace log box (380px tall) that fills with the selected log content. During active operations, new lines scroll into view automatically.
Confirm modal
A centered modal overlay with a dark semi-transparent background. The modal box has a red warning header "Erase disk and install?", explanatory text, a detail section showing the selected disk and configuration, and two buttons: grey "Cancel" and red "Yes, erase and install." The modal blocks all interaction until dismissed.
Troubleshooting
Port 8080 already in use
Another process is listening on 8080. Check with ss -tlnp | grep 8080.
Common culprit: a previous instance of kldload-webui that did not shut down cleanly.
Kill it with kill $(lsof -ti:8080) or restart the service with
systemctl restart kldload-webui. You can also start the backend on a
different port: kldload-webui --port 9090.
WebSocket connection failed
The browser shows "disconnected — retrying..." in the status bar. Check that the
backend is running: systemctl status kldload-webui. Check the backend log:
journalctl -u kldload-webui. If the backend crashed, it usually means the
websockets module is missing or incompatible. The build process pip-installs
a compatible version, but if you are running from source, install it manually:
pip3 install websockets. Requires version 11+ with the
websockets.http11 API.
No disks found
The disk dropdown shows "No disks found" or stays on "scanning..." The backend runs
lsblk -J to detect disks. If no disks appear: check that the disk controller
driver is loaded (lsmod | grep nvme or lsmod | grep ahci),
check lsblk output directly in a terminal, and verify the disk is not entirely
consumed by the live ISO (should not happen — the live boot device is excluded from
recommendations but still listed).
Install fails immediately
Check the installer log: cat /var/log/installer/kldload-installer.log.
Common causes: target disk is still mounted from a previous attempt (run
umount -R /target; zpool export rpool), ZFS kernel module failed to load
(Secure Boot enabled without MOK enrollment — reboot and enroll in MokManager),
or the target disk has active partitions from a previous OS (use "Wipe Disk" to zero it).
Browser shows blank page
The HTTP server part of the backend serves static files from the active edition directory.
If the page is blank, check that /usr/local/share/kldload-webui/active/index.html
exists. If it does not, the build did not copy the edition correctly. As a workaround, create
a symlink: ln -s /usr/local/share/kldload-webui/free /usr/local/share/kldload-webui/active.
NVIDIA option greyed out but GPU is present
The backend detects NVIDIA GPUs by running lspci and checking for "NVIDIA"
in the output. If the GPU is behind a bridge or uses a non-standard PCI ID, it might not
be detected. Check lspci | grep -i nvidia in a terminal. If the GPU appears
there but the web UI does not enable the option, the system_info response
might be cached — refresh the page to force a new query.
Install completes but system does not boot
Most boot failures are bootloader-related. kldload uses ZFSBootMenu as the bootloader.
Check that the EFI System Partition was created correctly: boot from the live ISO again,
import the pool (zpool import rpool), and inspect
/boot/efi/EFI/KLDload/. If Secure Boot is enabled, the ZFS module must be
signed — the MOK key should have been enrolled during the first live boot. Try
disabling Secure Boot in BIOS as a diagnostic step.
WebSocket works in Firefox but not Chrome
Both browsers are supported. If Chrome refuses the WebSocket connection, it might be
due to mixed content: if you access the page via HTTPS but the WebSocket URL is
ws:// (not wss://), Chrome blocks it. The frontend auto-detects
the protocol (location.protocol === 'https:' ? 'wss' : 'ws'), but if you
are behind a reverse proxy, ensure it passes the WebSocket upgrade correctly.
The most common support issue is Secure Boot + ZFS. If the live ISO boots but ZFS modules fail to load, it is almost always because Secure Boot is enabled and the MOK (Machine Owner Key) has not been enrolled. The live ISO includes a signed ZFS module, but the signing key needs to be trusted by the firmware. On first boot with Secure Boot, MokManager should prompt for enrollment. If it does not, or if enrollment was skipped, the ZFS module cannot load and the installer will fail at preflight.
The second most common issue is trying to install over a previous installation without cleaning up. If rpool already exists from a prior attempt, the installer cannot create it again. The "Wipe Disk" button in the web UI handles this — it zeros the disk, destroying all partition tables and ZFS labels. For manual cleanup: zpool destroy rpool, wipefs -a /dev/vda, sgdisk --zap-all /dev/vda.
How kldload's web UI compares
| Feature | kldload | Cockpit | Proxmox | Webmin |
|---|---|---|---|---|
| Dependencies | Python 3 + websockets | cockpit-ws + D-Bus + PAM + polkit | pveproxy + corosync + ceph-common + ... | Perl + 100+ modules |
| Source files | 2 files (~3,800 lines) | Hundreds of JS modules | Hundreds of Perl/JS files | Thousands of Perl files |
| ZFS native | Yes — first-class | Plugin only | Yes | Plugin only |
| Install wizard | Yes — multi-distro | No | Single distro | No |
| Offline install | Yes — darksites | N/A | No | N/A |
| WebSocket API | Yes — full | Partial (D-Bus bridge) | REST API | REST API |
| Unattended install | Answers file | N/A | Answer file | N/A |
| Golden image export | Yes — 5 formats | No | Template clone | No |
| Authentication | Local access only | PAM + optional TLS | PAM + TLS | PAM + TLS |
| Can read entire source | In one sitting | No | No | No |
The web UI is not trying to be Cockpit or Proxmox. It is not a general-purpose Linux management panel. It is the installer and the dashboard for kldload systems specifically. It knows about ZFS pools, boot environments, darksites, WireGuard meshes, and golden image exports because those are the things kldload builds. It does not try to manage Apache virtual hosts or MySQL databases because that is not what kldload is for.
The authentication model is intentionally simple: the web UI listens on localhost:8080. If you can reach the port, you can use the UI. On the live ISO, there is no authentication because you are sitting at the console. On an installed system, the web UI is behind the firewall — you access it through SSH port forwarding (ssh -L 8080:localhost:8080 admin@host) or a WireGuard tunnel. There is no login page because there is no need for one — if you can reach the machine, you already proved your identity to SSH or WireGuard.