a1e14c64c3
- Q1 → Ontotext GraphDB (SPARQL). docs/08 fully rewritten with RDF schema, example SPARQL queries (transitive deps via property paths, time-windowed topic counts, cross-pillar joins via nexa:vectorId). - Q2 → reuse qdrant_scientific with nexa_* collection prefix; docs/09 step 2 now points there explicitly. - Q3 → no OpenAI embeddings. Self-host on the docker host. Use TEI (HuggingFace text-embeddings-inference) — single Rust binary, ~500 MB image, OpenAI-compatible — instead of Ollama, since we only need embeddings. - docs/09 Phase-3.4 add-on simplified to a single Ontotext compose snippet (Neo4j option dropped) plus repo creation curl. - docs/11 Q3 marked resolved; new Q15 picks the model (bge-m3 vs nomic-embed) and adds the open question of whether SAIA already proxies an embedding model that would let us skip TEI entirely. - docs/03 + CLAUDE.md updated with the new decisions so future runs don't re-litigate.
236 lines
9.1 KiB
Markdown
236 lines
9.1 KiB
Markdown
# 09 — Deployment
|
||
|
||
Pragmatic deployment guide that **assumes the existing homelab** and adds only what's missing.
|
||
|
||
## What's already running (no action required)
|
||
|
||
Surveyed from Homepage / Dozzle / Proxmox / Zoraxy:
|
||
|
||
| Service | Host / port | URL |
|
||
|---------|-------------|-----|
|
||
| Memos | docker LXC 104 → `:5230` | `https://memos.nuclide.systems` |
|
||
| n8n | docker LXC 104 → `:5678` | `https://n8n.nuclide.systems` |
|
||
| LiteLLM (SAIA gateway) | docker LXC 104 → `:4000` | `https://ai.nuclide.systems` (proxies LobeHub UI :3210; API on :4000) |
|
||
| Nextcloud | LXC 105 | `https://nc.nuclide.systems` |
|
||
| ntfy | docker LXC 104 → `:7998` | `https://ntfy.nuclide.systems` |
|
||
| Karakeep (Hoarder) | docker LXC 104 → `:3090` | `https://hoarder.nuclide.systems` |
|
||
| Home Assistant | VM 100 (HAOS) | `https://ha.nuclide.systems` |
|
||
| Pocket-ID (OAuth/SSO) | docker LXC 104 → `:1411` | `https://id.nuclide.systems` |
|
||
| Vaultwarden | docker LXC 104 → `:11001` | `https://vault.nuclide.systems` |
|
||
| Backrest | LXC 103 | (internal) |
|
||
| AdGuard DNS | LXC 102 | (internal) |
|
||
| Zoraxy reverse proxy | LXC 108 → `192.168.1.4:8000` | TLS for *.nuclide.systems |
|
||
| `qdrant_scientific` (existing) | docker LXC 104 | **reused** — Nexa uses `nexa_*` collections in this instance |
|
||
|
||
The deployment task is **not** "spin up the stack" — most of the stack is already up. It is **wire Nexa across these services + add the small bits that are missing**.
|
||
|
||
## What's missing for Nexa
|
||
|
||
1. **Qdrant collection** for Nexa (`nexa_knowledge`) inside the existing `qdrant_scientific` instance — vector dim follows Q15 (1024 for `bge-m3`, 768 for `nomic-embed-text`).
|
||
2. **TEI** (HF text-embeddings-inference) on the docker host for self-hosted embeddings (LiteLLM key is **not** authorised for OpenAI embeddings — see [11/Q3+Q15](./11-open-questions.md)). Lighter than Ollama: single Rust binary, ~500 MB image, no LLM runtime.
|
||
3. **n8n workflows** (`./nexa-core/n8n-workflows/`) imported into the running n8n.
|
||
4. **Nextcloud lists & calendars** for Work / Personal / Shopping / Wishes (auto-discovered via `#nexa:config`).
|
||
5. **Memos webhook → n8n** wired through the Memos config.
|
||
6. **LiteLLM virtual key** for the `nexa` user with chat-only access (no embeddings — handled by Ollama).
|
||
7. **A Zoraxy host entry** is *not* needed — Memos / n8n / LiteLLM are already proxied.
|
||
8. **(Phase 3.4) Ontotext GraphDB** for the SPARQL pillar — see add-on at the bottom of this doc.
|
||
|
||
---
|
||
|
||
## Step 1 — Secrets
|
||
|
||
Copy `nexa-core/.env.example` → `nexa-core/.env` and fill **only** the secrets:
|
||
|
||
```bash
|
||
cd nexa-core
|
||
cp .env.example .env
|
||
$EDITOR .env # MEMOS_API_KEY, SAIA_API_KEY, NC_APP_PASSWORD, QDRANT_API_KEY
|
||
```
|
||
|
||
The `.env` is **only used at bootstrap time**. Everything else (list IDs, calendar IDs, collection sizes) is discovered at runtime via `#nexa:config` (see [05](./05-command-system.md)). No secrets should ever live in n8n workflow JSON — use n8n credentials instead.
|
||
|
||
## Step 2 — Qdrant collection
|
||
|
||
```bash
|
||
# adjust QDRANT_HOST in .env first
|
||
source nexa-core/.env
|
||
|
||
curl -X PUT "$QDRANT_HOST/collections/nexa_knowledge" \
|
||
-H "Content-Type: application/json" \
|
||
-H "api-key: $QDRANT_API_KEY" \
|
||
-d @nexa-core/config/qdrant_schema.json
|
||
```
|
||
|
||
> Targets the existing `qdrant_scientific` instance — just an extra collection, no new container.
|
||
> The schema file's `vectors.size` must match the embedding model picked in [Q15](./11-open-questions.md): **1024** for `bge-m3`, **768** for `nomic-embed-text-v1.5`.
|
||
|
||
## Step 3 — Self-hosted embeddings (TEI)
|
||
|
||
Use HuggingFace **text-embeddings-inference** — single Rust binary, ~500 MB image, OpenAI-compatible API, loads exactly one model. Lighter than Ollama because there's no LLM runtime, no GGUF loader, no model registry.
|
||
|
||
```bash
|
||
# on the docker host (LXC 104)
|
||
docker run -d --name nexa-embed \
|
||
--restart unless-stopped \
|
||
-p 127.0.0.1:8080:80 \
|
||
-v tei-data:/data \
|
||
ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 \
|
||
--model-id BAAI/bge-m3
|
||
```
|
||
|
||
Memory budget: ~1.1 GB resident (bge-m3 is ~1 GB + ~100 MB overhead). First start downloads the model into the named volume; subsequent restarts are instant.
|
||
|
||
Register it inside LiteLLM (admin UI → Models) with the OpenAI-compatible adapter:
|
||
|
||
- model name: `nexa-embed`
|
||
- provider: `openai`
|
||
- model: `bge-m3`
|
||
- api_base: `http://nexa-embed:80/v1`
|
||
- api_key: any non-empty string (TEI ignores it)
|
||
|
||
Now n8n only ever talks to LiteLLM and the model is swappable without touching workflows.
|
||
|
||
## Step 4 — LiteLLM virtual key
|
||
|
||
In the LiteLLM admin UI (`ai.nuclide.systems`):
|
||
|
||
1. Create user `nexa`.
|
||
2. Issue a virtual key with access to:
|
||
- one chat model (already-available model from your SAIA gateway).
|
||
- the `nexa-embed` model from Step 3.
|
||
3. Paste the key into `SAIA_API_KEY` in `.env`.
|
||
|
||
## Step 4 — n8n workflows
|
||
|
||
Import the JSON exports — credentials are filled inside n8n, not in the JSON:
|
||
|
||
```bash
|
||
# n8n personal access token from the n8n UI: Settings → API
|
||
N8N_URL=https://n8n.nuclide.systems
|
||
N8N_TOKEN=... # from the n8n UI
|
||
|
||
for f in nexa-core/n8n-workflows/phase-1/*.json \
|
||
nexa-core/n8n-workflows/phase-2/*.json; do
|
||
curl -X POST "$N8N_URL/api/v1/workflows" \
|
||
-H "X-N8N-API-KEY: $N8N_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
--data-binary "@$f"
|
||
done
|
||
```
|
||
|
||
Inside n8n, attach credentials to the imported nodes:
|
||
|
||
- **Memos** → HTTP header `Authorization: Bearer $MEMOS_API_KEY`
|
||
- **LiteLLM** → header `Authorization: Bearer $SAIA_API_KEY`
|
||
- **Nextcloud** → app password
|
||
- **Qdrant** → header `api-key: $QDRANT_API_KEY`
|
||
|
||
Activate each workflow individually after smoke-test.
|
||
|
||
## Step 5 — Memos webhook
|
||
|
||
In the Memos admin UI, set the webhook URL to the production address of the discovery workflow:
|
||
|
||
```
|
||
https://n8n.nuclide.systems/webhook/memos
|
||
```
|
||
|
||
The same URL is the one the workflow exposes; verify with:
|
||
|
||
```bash
|
||
curl -i https://n8n.nuclide.systems/webhook/memos
|
||
# expect 200 / 405, never 404
|
||
```
|
||
|
||
## Step 6 — Bootstrap commands via Memos
|
||
|
||
Create a memo with body `#nexa:config` — the discovery workflow:
|
||
|
||
1. Lists Nextcloud Tasks lists, picks Work / Personal / Shopping / Wishes by name.
|
||
2. Counts existing Qdrant points in `nexa_knowledge`.
|
||
3. Verifies LiteLLM reachability + lists available models.
|
||
4. Replies as a comment with a **runtime-config snapshot** that's stored as Qdrant metadata (`_config` namespace) and as `nexa-core/config/runtime_config.json` (gitignored).
|
||
|
||
After this point, `.env` is read-once. Subsequent runs read config from Qdrant.
|
||
|
||
## Step 7 — Smoke tests
|
||
|
||
```bash
|
||
# (1) Memos round-trip — should produce a comment within ~5 s
|
||
curl -X POST https://memos.nuclide.systems/api/v1/memos \
|
||
-H "Authorization: Bearer $MEMOS_API_KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"content":"- [ ] testing the router #nexa"}'
|
||
|
||
# (2) Classification dry-run
|
||
curl -X POST https://memos.nuclide.systems/api/v1/memos \
|
||
-H "Authorization: Bearer $MEMOS_API_KEY" \
|
||
-d '{"content":"#nexa:route-test buy milk"}'
|
||
|
||
# (3) RAG test (requires at least one indexed memo/note)
|
||
curl -X POST https://memos.nuclide.systems/api/v1/memos \
|
||
-H "Authorization: Bearer $MEMOS_API_KEY" \
|
||
-d '{"content":"#nexa:ask what is the goal of nexa?"}'
|
||
```
|
||
|
||
## Step 8 — Reverse proxy
|
||
|
||
Already done — Zoraxy at `192.168.1.4:8000` terminates TLS for `*.nuclide.systems` and forwards to docker LXC 104 (`192.168.1.40`). **No new entry is required for Nexa**: every service Nexa talks to already has a host entry.
|
||
|
||
## Step 9 — Backups
|
||
|
||
Already covered by Backrest (LXC 103). Add:
|
||
|
||
- **n8n workflows** → `nexa-core/scripts/backup_workflows.sh` (already present) into a Backrest schedule.
|
||
- **Qdrant snapshots** → schedule a daily `POST /collections/nexa_knowledge/snapshots` and rsync to S3 (`s3.nuclide.systems`). Add as a Backrest pre-hook on the docker host.
|
||
|
||
For deeper detail: [10 — Operations](./10-operations.md).
|
||
|
||
---
|
||
|
||
## Phase add-on: Ontotext GraphDB (Phase 3.4)
|
||
|
||
Defer until 3.1–3.3 ship.
|
||
|
||
```yaml
|
||
# nexa-core/docker-compose.graph.yml
|
||
services:
|
||
graphdb:
|
||
image: ontotext/graphdb:10.7.0
|
||
container_name: nexa-graphdb
|
||
ports: ["127.0.0.1:7200:7200"]
|
||
environment:
|
||
GDB_JAVA_OPTS: "-Xmx4g -Xms1g"
|
||
volumes:
|
||
- ./data/graphdb:/opt/graphdb/home
|
||
restart: unless-stopped
|
||
```
|
||
|
||
After first start, create the repository (one-time):
|
||
|
||
```bash
|
||
curl -X POST http://localhost:7200/rest/repositories \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"id": "nexa_knowledge",
|
||
"title": "Nexa Knowledge Graph",
|
||
"type": "graphdb",
|
||
"params": {
|
||
"ruleset": {"value": "rdfsplus-optimized"},
|
||
"baseURL": {"value": "https://nuclide.systems/nexa/"}
|
||
}
|
||
}'
|
||
```
|
||
|
||
Optional Zoraxy entry `graph.nuclide.systems` → `192.168.1.40:7200` if you want the SPARQL Workbench in a browser; otherwise n8n talks to it on the docker network at `http://nexa-graphdb:7200`.
|
||
|
||
For schema and example queries: [08-graphrag-architecture](./08-graphrag-architecture.md).
|
||
|
||
---
|
||
|
||
## Step-back / rollback
|
||
|
||
- **Disable any Nexa workflow** in n8n — deactivates the side effect immediately, Memos webhooks become no-ops.
|
||
- **Drop a Qdrant collection** — `curl -X DELETE $QDRANT_HOST/collections/nexa_knowledge -H "api-key: $QDRANT_API_KEY"`.
|
||
- **Re-discover** — `#nexa:reset-config` then `#nexa:config`.
|