operate
Keep kolu running.
kolu’s home-manager module runs the web server as a systemd user service on Linux or a launchd LaunchAgent on macOS. Both supervisors restart a failed server, while padi and kaval keep workspace state and live shells below it.
Enable the service
Import the module and select the package for the current system:
{ pkgs, kolu, ... }:
{
imports = [ kolu.homeManagerModules.default ];
services.kolu = {
enable = true;
package = kolu.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
}
Apply the home-manager configuration as usual, then open
http://127.0.0.1:7681. The module also puts the
matching kolu, padi-tui, and kaval-tui commands on PATH.
A generation built this way carries every agent kolu can send to a remote
host, so a first connect copies binaries this machine already
holds instead of fetching them from a cache or asking the remote machine to
build them. Nothing to warm, nothing to run before switching. The set comes
from services.kolu.agentPackages, which the module’s flake defaults to the
agents kolu is allowed to provision — override it only to pin particular
builds.
The complete, build-tested flake is in
nix/home/example.
CI boots its systemd configuration in a NixOS VM and builds its launchd
activation package on macOS.
On a headless NixOS box, also set users.users.<you>.linger = true in the
system config so the user systemd — and this service — starts at boot without
a login.
Ubuntu (and other non-NixOS Linux)
Same module, applied with standalone home-manager. Install Nix with flakes
first (NixOS Asia guide), then a flake whose
homeConfigurations output imports the module:
{
description = "kolu on this box";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
kolu.url = "github:juspay/kolu";
};
outputs = { nixpkgs, home-manager, kolu, ... }:
let
system = "x86_64-linux"; # aarch64-linux on ARM
in {
homeConfigurations.you = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.${system};
modules = [
kolu.homeManagerModules.default
({ pkgs, ... }: {
home.username = "you";
home.homeDirectory = "/home/you";
home.stateVersion = "24.11";
services.kolu = {
enable = true;
package = kolu.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
})
];
};
};
}
Replace you and x86_64-linux to match the box. From that directory:
nix run home-manager -- switch --flake .#you
sudo loginctl enable-linger you
Linger keeps the user systemd running after SSH disconnects — without it the service dies when you log out, which is the opposite of a headless box. Then open http://127.0.0.1:7681. Address, origins, and logs below apply unchanged. The From a Mac tutorial walks this as the first step of the Mac-window setup.
Choose the address
The service listens on loopback port 7681 by default. Change either value in
the module:
services.kolu = {
host = "0.0.0.0";
port = 8080;
};
Bind a different address
A one-off nix run (or kolu web) listens on loopback too. To expose it on
your LAN, pass an address and port after --:
nix --refresh run github:juspay/kolu -- web --bind 0.0.0.0 --port 8080
Open the address you chose. For another device, prefer the private HTTPS setup in From a Mac or From another device; plain LAN HTTP does not unlock install prompts, badges, or notifications.
First run compiling? Enable the binary cache
Every kolu commit is pre-built and pushed to a public binary cache,
https://cache.nixos.asia/oss. The flake declares it, but Nix only honors a
flake-declared cache if you accept it and your user is
trusted
by the Nix daemon — and on a standard multi-user install it isn’t, so the
setting is dropped with a one-line warning
(ignoring untrusted flake configuration setting 'extra-substituters') and the
first run compiles kolu from source, including downloading a Rust toolchain
for one of its helper binaries.
To download pre-built binaries instead, add the cache to the system Nix
configuration (/etc/nix/nix.conf):
extra-substituters = https://cache.nixos.asia/oss
extra-trusted-public-keys = oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU=
Or, on NixOS:
nix.settings = {
extra-substituters = [ "https://cache.nixos.asia/oss" ];
extra-trusted-public-keys = [ "oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU=" ];
};
After editing nix.conf, restart the daemon (sudo systemctl restart nix-daemon on Linux, sudo launchctl kickstart -k system/org.nixos.nix-daemon on macOS).
Machines you add as another machine can inherit the cache
through kolu: provisioning copies the agent’s binaries out of this machine’s
store onto the host, cross-architecture included. That copy lands only if the
host trusts it — your ssh user is in the host’s trusted-users, or the host
trusts the cache’s signing key. When it doesn’t, kolu says so in the connect
log and the host builds the agent itself; the fix is the same nix.conf block
above, applied on that machine.
Whether this machine has those binaries to copy depends on how you installed
kolu. If you run kolu as a service through the home-manager module, it
always does: the installed generation carries every agent closure kolu can
provision, so the cache above is only about kolu’s own first build, never
about a remote connect. If you launch kolu with nix run or a plain nix profile install, the agent closure is fetched on demand — from this cache, and
from source if the cache doesn’t have it yet.
Allow a reverse-proxy origin
kolu accepts its own browser origin by default. A reverse proxy or
tailscale serve can present a public browser origin that differs from the
Host forwarded to kolu. Add that exact origin:
services.kolu.allowedOrigins = [
"https://box.tailnet.ts.net"
];
This sets KOLU_ALLOWED_ORIGINS for both the WebSocket and HTTP RPC paths. The
architecture page explains why the gate
exists.
Read service logs
On Linux, use the systemd user journal:
systemctl --user status kolu
journalctl --user -u kolu -f
On macOS, launchd writes the two streams to:
~/Library/Logs/kolu.out.log
~/Library/Logs/kolu.err.log
Both work because a service manager owns the server’s output. Started by
hand, nothing does — kolu web writes to your terminal and that is the only
copy, so redirect it if the run is meant to outlive the shell:
kolu web > ~/kolu-server.log 2>&1
This is the log that records why a host’s daemon was stopped or replaced; Operations lists the full set and which question each answers.
Crashes and startup failures remain visible after the process exits.
Capture memory diagnostics
If the server or kaval daemon grows without bound, set an absolute diagnostics directory:
services.kolu.diagnostics.dir =
"${config.home.homeDirectory}/.local/state/kolu-diagnostics";
Each restart gets a timestamped subdirectory containing:
- a baseline heap snapshot after five minutes;
- periodic memory statistics;
- automatic snapshots near the V8 heap limit; and
- snapshots requested with
SIGUSR2.
To capture a snapshot on demand:
kill -USR2 <pid>
The snapshot lands in the current run’s diagnostics directory. Compare snapshots offline with MemLab to find what retains the growing objects.
Diagnostics are disabled by default and add no overhead until dir is set.
Browser-tab or GPU growth has a different checklist in
Troubleshooting.
Stop it, roll it back, or test beside it
Stopping the whole stack by hand — the daemons included — is not systemctl stop
alone, and it is what a rollback to an older release needs. Running a second kolu
on a machine that already has one needs its own state directories, or the test
instance takes production’s daemons over. Both procedures, and the vocabulary
they share, are in Operations.