8.2 KiB
Hosting KisAssistant on a NixOS server
End-to-end guide: build the flake, wire the module into configuration.nix,
secure it with agenix/sops-nix, and put it behind nginx with automatic TLS.
Contents:
- Prerequisites
- Add the flake to your system inputs
- Secrets (
APP_PASSWORD, API keys) - Enable the service
- TLS reverse proxy (
enableNginx) - First deploy & smoke test
- Operations: logs, backups, updates
- Module option reference
1. Prerequisites
- A NixOS machine (unstable channel or a flake lock close to it) with
networking+firewallunder your control. - Bun is not installed on the server — the flake wraps the runtime.
- DNS: an A/AAAA record for your domain (e.g.
chat.kisakay.com) pointing at the server. ACME (Let's Encrypt) needs port 80/443 reachable during issuance. - Ollama reachable from the server (e.g.
http://10.66.66.4:11434on your LAN/tailscale).
2. Add the flake to your system inputs
In your system flake (or flake.nix of your NixOS config):
{
inputs.kisassistant.url = "http://git.kisakay.com/k/chat"; # adjust to real repo path
outputs = { self, nixpkgs, kisassistant, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
kisassistant.nixosModules.default
];
};
};
}
If you don't use flakes for your system config, import the module by path:
imports = [ /etc/nixos/kisassistant/module.nix ];
# and point the package at a build:
services.kisassistant.package =
(builtins.getFlake "git+http://git.kisakay.com/k/chat").packages.x86_64-linux.default;
The frontend is compiled offline inside the Nix build with Bun from
frontend/bun.lock(fixed-output deps fetch). Ifnix buildever fails with abunDepsHashmismatch, paste thegot: sha256-…value from the error intobunDepsHashinflake.nixand rebuild. This is normal after dependency bumps.
3. Secrets (APP_PASSWORD, API keys)
Never put secrets in the Nix store. Use agenix or sops-nix. Minimal agenix setup:
# on your machine, in the secrets repo of the host
npx agenix generate kisassistant-password -a $(readlink -f /etc/ssh/ssh_host_ed25519_key.pub)
# write the admin key into the file, e.g.:
echo "$(openssl rand -base64 24)" | agenix -e kisassistant-password.age # keep a copy!
Then on the host:
age.secrets.kisassistant-password.file = /etc/nixos/secrets/kisassistant-password.age;
The module consumes it as passwordFile and injects it into the service via
systemd LoadCredential — the file is read at runtime only.
4. Enable the service
services.kisassistant = {
enable = true;
# REQUIRED — the module refuses to evaluate without it.
package = kisassistant.packages.${pkgs.system}.default;
# admin key file (agenix/sops-nix path on the host)
passwordFile = config.age.secrets.kisassistant-password.path;
# storage (created by systemd StateDirectory, owned by the service user)
# SQLite: /var/lib/kisassistant/data
# CDN: /var/lib/kisassistant/cdn
dataDir = "/var/lib/kisassistant";
# LLM backends
ollamaHost = "http://10.66.66.4:11434";
extraEnv = {
MISTRAL_ENABLED = "true"; # non-secret toggles only — see the note below
};
};
The module automatically sets for the systemd unit:
| Env | Value |
|---|---|
NODE_ENV |
production |
DATA_DIR |
/var/lib/kisassistant/data |
CDN_DIR |
/var/lib/kisassistant/cdn |
DRIVER_DEBUG |
false |
PORT / HOST |
127.0.0.1:3000 (configurable) |
The module also puts tesseract on the service PATH, so the OCR platform
tool (POST /api/tools/ocr, image attachments) works out of the box.
OCR_LANG / OCR_MAX_CHARS can be overridden via extraEnv.
API driver keys:
extraEnvvalues land in the unit'sEnvironment=, which is world-readable in the systemd unit file. For driver API keys preferEnvironmentFile=via a drop-in (or pass them from agenix withsystemd.services.kisassistant.serviceConfig.EnvironmentFile), e.g.services.kisassistant.extraEnv = { MISTRAL_ENABLED = "true"; };plus a drop-in containing the key.
5. TLS reverse proxy (enableNginx)
The conventional switch is enableNginx — a tri-state:
| Value | Behaviour |
|---|---|
true |
nginx vhost + ACME configured (requires domain) |
false |
nginx never touched, even if domain is set |
null (default) |
legacy: nginx configured iff domain is set |
services.kisassistant = {
enableNginx = true;
domain = "chat.kisakay.com"; # vhost name + ACME cert name
# host/port default to 127.0.0.1:3000
};
# Required once per host for ACME (Let's Encrypt ToS):
security.acme.acceptTerms = true;
security.acme.defaults.email = "[email protected]";
# Keep the backend loopback-only and let nginx own 80/443:
networking.firewall.allowedTCPPorts = [ 80 443 ];
The generated vhost: forceSSL, enableACME, recommendedProxySettings, and
proxy_buffering off on / so SSE token streaming (/api/chat) is never
buffered.
Own nginx instead? Set enableNginx = false (or omit domain) and write
your own vhost; proxy to http://127.0.0.1:3000 and remember
proxy_buffering off; for / (or at least /api/chat).
6. First deploy & smoke test
sudo nixos-rebuild switch --flake .#myhost
Check the unit and the proxy:
systemctl status kisassistant
journalctl -u kisassistant -f # boot log (APP_PASSWORD errors are fatal)
curl -I https://chat.kisakay.com/ # 200 via nginx
curl -s https://chat.kisakay.com/api/models -H "Authorization: Bearer $TOKEN"
Log in through the web UI as admin with the APP_PASSWORD from your secret
file, then create user accounts from the Accounts panel (each gets a
one-time ka_… key). See docs/OPERATIONS.md for account management.
If port 3000 is used by something else on the host, change
services.kisassistant.port and the proxy target follows automatically.
7. Operations: logs, backups, updates
Logs — per-driver debug lines are off in production
(DRIVER_DEBUG=false); flip temporarily with:
sudo systemctl edit kisassistant # [Service] Environment=DRIVER_DEBUG=true
sudo systemctl restart kisassistant
Backups — everything is in /var/lib/kisassistant:
# SQLite is in WAL mode; prefer the backup API over raw copies:
sqlite3 /var/lib/kisassistant/data/kisassistant.db ".backup '/backup/kisassistant.db'"
systemctl stop kisassistant && tar -C /var/lib -czf /backup/kisassistant-$(date +%F).tgz kisassistant && systemctl start kisassistant
Updates — the usual flake flow:
nix flake update kisassistant # or update the rev you pin
sudo nixos-rebuild switch --flake .#myhost
sudo nix store gc # keep the previous generation around as needed
After dependency bumps the first build may fail with a bunDepsHash mismatch
— paste the reported hash into flake.nix (see section 2).
8. Module option reference
| Option | Type / default | Description |
|---|---|---|
enable |
bool | the switch |
package |
null (required) | flake package, e.g. kisassistant.packages.${pkgs.system}.default |
port |
port, 3000 |
backend listen port |
host |
str, 127.0.0.1 |
backend bind address (keep loopback behind nginx) |
dataDir |
str, /var/lib/kisassistant |
state root (data/ + cdn/ live inside) |
user |
str, kisassistant |
system user/group for the service |
passwordFile |
null | path | file containing APP_PASSWORD (agenix/sops-nix) |
ollamaHost |
str, http://localhost:11434 |
Ollama endpoint |
domain |
null | str | nginx vhost name, required when enableNginx = true |
enableNginx |
null | bool, null |
true/false force the bundled reverse proxy on/off; null = nginx iff domain set (legacy) |
extraEnv |
attrs of str, {} |
extra environment (non-secret values only) |