232 lines
8.2 KiB
Markdown
232 lines
8.2 KiB
Markdown
# 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:
|
|
|
|
1. [Prerequisites](#1-prerequisites)
|
|
2. [Add the flake to your system inputs](#2-add-the-flake-to-your-system-inputs)
|
|
3. [Secrets (`APP_PASSWORD`, API keys)](#3-secrets-app_password-api-keys)
|
|
4. [Enable the service](#4-enable-the-service)
|
|
5. [TLS reverse proxy (`enableNginx`)](#5-tls-reverse-proxy-enablenginx)
|
|
6. [First deploy & smoke test](#6-first-deploy--smoke-test)
|
|
7. [Operations: logs, backups, updates](#7-operations-logs-backups-updates)
|
|
8. [Module option reference](#8-module-option-reference)
|
|
|
|
---
|
|
|
|
## 1. Prerequisites
|
|
|
|
- A NixOS machine (unstable channel or a flake lock close to it) with
|
|
` networking` + `firewall` under 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:11434` on your
|
|
LAN/tailscale).
|
|
|
|
## 2. Add the flake to your system inputs
|
|
|
|
In your system flake (or `flake.nix` of your NixOS config):
|
|
|
|
```nix
|
|
{
|
|
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:
|
|
|
|
```nix
|
|
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). If `nix build` ever fails with
|
|
> a `bunDepsHash` mismatch, paste the `got: sha256-…` value from the error into
|
|
> `bunDepsHash` in `flake.nix` and rebuild. This is normal after dependency
|
|
> bumps.
|
|
|
|
## 3. Secrets (`APP_PASSWORD`, API keys)
|
|
|
|
Never put secrets in the Nix store. Use [agenix](https://github.com/ryantm/agenix)
|
|
or [sops-nix](https://github.com/Mic92/sops-nix). Minimal agenix setup:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```nix
|
|
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
|
|
|
|
```nix
|
|
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**: `extraEnv` values land in the unit's `Environment=`,
|
|
> which is world-readable in the systemd unit file. For driver API keys prefer
|
|
> `EnvironmentFile=` via a drop-in (or pass them from agenix with
|
|
> `systemd.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 |
|
|
|
|
```nix
|
|
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
|
|
|
|
```bash
|
|
sudo nixos-rebuild switch --flake .#myhost
|
|
```
|
|
|
|
Check the unit and the proxy:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
sudo systemctl edit kisassistant # [Service] Environment=DRIVER_DEBUG=true
|
|
sudo systemctl restart kisassistant
|
|
```
|
|
|
|
**Backups** — everything is in `/var/lib/kisassistant`:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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) |
|