kolu
Docs

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.