update
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
# 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 = "you@kisakay.com";
|
||||
|
||||
# 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) |
|
||||
Reference in New Issue
Block a user