User's Nextcloud rollout already hit the classic trap of unprivileged-LXC write attempts against an NFS share, so don't repeat that pattern for Nexa. Updated docs to: - Use SMB volumes mounted directly as docker volumes (driver: local, type: cifs) with explicit username/uid/gid — bypasses LXC uid-mapping entirely. Concrete YAML in docs/09 step 3 against //192.168.1.31/nexa/<volume>. - Keep the host-side /mnt/pve/unas NFS mount for admin-only use (rsync, manual snapshot copies). Don't bind-mount it into containers. - docs/12 #27 reordered to lead with SMB targets (qdrant, graphdb, tei-cache, snapshots) and only mention NFS for host admin. - CLAUDE.md storage block flipped to "SMB = default per-container storage; NFS = host-side admin only". - Also captured: deployments go through the Dockge UI (LXC is the Dockge flavor of helper-scripts), not raw `docker compose up`. - New optimization #30 about the local mail spool nag on the docker LXC ("You have new mail." at login).
11 KiB
12 — Optimization Opportunities
Observations from the running infrastructure. Each item is independent — accept, defer, or reject.
For Nexa directly
- Reuse, don't redeploy. The earlier
DEPLOYMENT.mdwould have spun a second Memos / n8n / Qdrant. The current homelab already runs all three. The new 09-deployment treats these as pre-existing — keeps the config minimal and avoids port collisions. - Use LiteLLM virtual keys per logical caller. Today there's one SAIA key. Issuing one key per workflow (
nexa-router,nexa-embed,nexa-digest) lets you set different per-key rate/cost limits and disable a single workflow without rotating everything. - Use n8n's credential objects, never inline secrets. The current workflows under
nexa-core/n8n-workflows/phase-1/*.jsonshould be reviewed — if any headerAuthorizationis hardcoded, replace with credential references before importing. - Centralise system alerts on a single ntfy topic (
nexa.system). Backrest, Proxmox notifications, n8n failure-webhook and the Octoprint Exited state all go to that topic; one Memos system memo aggregates them. - Defer graph DB until Phase 3.4. Qdrant alone covers ~80% of the assistant's daily value. The graph DB is justified once you actually need dependency analysis or critical-path queries.
- Auto-export n8n workflows.
nexa-core/scripts/backup_workflows.shalready exists. Schedule it inside the n8n container (cron) and let itgit commit && git push— this is the cheapest disaster recovery.
For the wider homelab (out of scope but worth noting)
- AI gateway naming.
ai.nuclide.systemscurrently proxies LobeHub (a chat UI on:3210), while the LiteLLM API lives on:4000. For Nexa, point n8n directly at LiteLLM (http://192.168.1.40:4000over the docker net — no public TLS hop needed) to save latency and isolate from UI restarts. - MCP servers consolidation. Dozzle shows
crawl4ai-mcp,markitdown-mcp,papersearch-mcprunning individually. They're all MCP servers — Nexa Phase-3 could pull from these via LiteLLM's MCP support to enrich the embedding pipeline (e.g. fetch + markitdown a Karakeep link before embedding). - Backup the n8n SQLite file — Backrest covers
/home/node/.n8nif added; today the only "backup" is the workflow JSON which omits credentials and execution history. - Pocket-ID SSO in front of n8n would let you remove n8n basic-auth and unify session management across the whole stack. One-time setup, large UX win.
- Vaultwarden as the secret store for Nexa secrets (
SAIA_API_KEY,MEMOS_API_KEY, …) — read at bootstrap via the Bitwarden CLI from inside the docker host. Removes the need for a.envon disk. - AdGuard as DNS-based control plane. Since AdGuard is the resolver for the LAN, you can rewrite
*.nuclide.systemsto192.168.1.4(Zoraxy) internally and avoid a hairpin via the WAN — already the case if AdGuard rewrite rules are set, worth verifying. - Disk usage on LXC 104 is 47.7 % (Proxmox). Monitor; n8n execution logs and Dozzle history are the usual culprits. Setting
EXECUTIONS_DATA_PRUNE=trueandEXECUTIONS_DATA_MAX_AGE=168(7 days) on n8n keeps it bounded. - Obsidian plugin embeddings collide with Nexa's. The vault already runs Obsidian Copilot (
.copilot,.copilot-index) and Smart Connections / Smart Composer (.smart-env, ~13 MB). They each embed the same notes into their own vector stores — three indexes for the same content. Nexa's value is the cross-source index (memos + mail + obsidian + RDF graph), so it has to embed independently, but the plugins could be retired once Nexa's RAG is satisfying. Track separately, decide later. - Real-time Obsidian sync via
notify_push. Phase 3.1 polls WebDAV every 15 min (Q4 resolution). Once that works, swap to Nextcloud'snotify_pushapp for sub-second propagation. One-line workflow change in n8n. assets/is 186 MB of binaries in the Obsidian vault — worth a glance to confirm it's mostly images (Phase-3.2 visual queue) rather than something that should live in Nextcloud Files proper.
Proxmox host (NUC 14 Pro) tuning
Observed from the node summary: 22 threads, 62 GiB RAM (32 GiB used, ~24 GiB of which is ZFS ARC), 1.64 TiB disk (0.35% used), load avg <2.0, IO delay 0.04%, kernel 6.17.13-4-pve, PVE 9.1.9, EFI. Suggestions in priority order:
- Cap ZFS ARC. Default is 50% of RAM (~31 GiB); current actual ~24 GiB. For a node that runs services rather than a pure storage box, capping at 8–12 GiB frees ~12–16 GiB for guests without measurable IO impact (disk is 1.64 TiB and 0.35% used — there's nothing hot to cache):
Reboot or
echo 'options zfs zfs_arc_max=8589934592' > /etc/modprobe.d/zfs.conf # 8 GiB update-initramfs -uecho 8589934592 > /sys/module/zfs/parameters/zfs_arc_maxto apply live. - Enable KSM (Kernel Same-page Merging). With ~40 docker containers + several LXCs, KSM typically frees 1–3 GiB by deduplicating identical memory pages. Currently
KSM sharing: 0 Bin the summary. PVE hasksmtunedavailable —systemctl enable --now ksmtuned. - Suppress the
pve-no-subscriptionrepository warning — either accept it (it's a homelab) and apply thepve-no-subscription-warningpolyfill, or move to the enterprise repo. Pure cosmetic, but the orange banner in the UI is noise. - Swap is 31 GiB on a 62 GiB box with ZFS root — almost certainly oversized. Drop
vm.swappinessto 10 (sysctl -w vm.swappiness=10+ persist) so swap is only used under genuine pressure, and consider shrinking the swap volume if disk-layout permits. - Verify scheduled ZFS scrub is enabled. PVE ships
zfs-scrub-monthly@.timer—systemctl list-timers | grep zfsto confirm. Cheap insurance on a 1.6 TiB pool. - SMART monitoring on the NVMe.
smartctl -a /dev/nvme0should be regularly polled; PVE's notification target can ntfy on degradation. Combine with the existingnexa.systemntfy topic (optimization #4) so disk-health alerts land in the same Memos system feed as everything else. - NTP source via AdGuard. AdGuard already resolves DNS for the LAN; pointing the host's
systemd-timesyncdatpool.ntp.orgresolved through AdGuard avoids any external dependency for time. One-line change in/etc/systemd/timesyncd.conf. fstrim.timerenabled for the SSD pool — verify withsystemctl status fstrim.timer. Default-on in modern PVE, but quick to confirm.- Watchdog config is irrelevant for a single-node setup (HA is the use case), so leave the default. Mentioned only so future agents don't add it speculatively.
Docker LXC (104) — observations & wins
Confirmed allocation: 16 CPU, 31.25 GiB RAM (7.86 GiB used / 25%), 8 GiB swap (idle), 200 GiB boot disk at 47.7% used — disk pressure outranks RAM pressure.
-
Fix the Intel iGPU passthrough. The container's own notes flag the binding as "likely failing". Once
/dev/dri/{card0,renderD128}is visible inside the LXC, both TEI andinfinitycan run embeddings on the Arc iGPU via OpenVINO / IPEX-LLM — typically 5–10× faster than CPU. Equally, Immich's CLIP can be GPU-accelerated. The required config in/etc/pve/lxc/104.conf:lxc.cgroup2.devices.allow: c 226:0 rwm lxc.cgroup2.devices.allow: c 226:128 rwm lxc.cgroup2.devices.allow: c 29:0 rwm lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file lxc.idmap: u 0 100000 65536 lxc.idmap: g 0 100000 65536 lxc.idmap: g 44 44 1 # video group on host lxc.idmap: g 104 104 1 # render group on hostThen inside the container:
usermod -aG video,render <docker-user>and run TEI with--device cudareplaced by the OpenVINO build (text-embeddings-inference:cpu-1.5-openvino). Not needed for Phase-3.1's volume but a clean upgrade path. -
Push persistent data to UNAS via SMB-backed docker volumes. The LXC also has a Proxmox-level NFS mount at
/mnt/pve/unas(19.4 TiB / 16.3 TiB free), but the user has already lived through the unprivileged-LXC + NFS + per-container-write trap with Nextcloud. For Nexa volumes the default is SMB, mounted directly as a docker volume (driverlocal, typecifs) with explicitusername=/uid=/gid=— that bypasses LXC uid-mapping entirely. Targets:- Qdrant data dir (
qdrant_scientific) → SMB share//192.168.1.31/nexa/qdrant. Move it before the visual collection lands and starts adding GBs. - GraphDB repo (Phase 3.4) →
//192.168.1.31/nexa/graphdb. The 4-GB heap is in RAM, but the on-disk RDF store will grow. - Embedding model caches (TEI/infinity) →
//192.168.1.31/nexa/tei-cache— keeps the boot disk free of multi-GB model files. - Daily Qdrant snapshots →
//192.168.1.31/nexa/snapshots/qdrant/<date>/before rsync to S3 (Phase 3.3 — Q12 still open). With UNAS in the path, S3 becomes a colder tier rather than the only copy. - n8n executions retention still useful:
EXECUTIONS_DATA_PRUNE=true,EXECUTIONS_DATA_MAX_AGE=168. - Monthly
docker image prune --all --filter "until=720h"to clear dangling layers.
Credentials: store
UNAS_USER/UNAS_PASSin Vaultwarden (12/#13) rather than a.envon disk; inject via a secrets agent or per-stack.envthat is itself stored on the SMB share with restricted ACLs. The host-side/mnt/pve/unasNFS mount stays useful for admin tasks (rsync to/from snapshots, manual backups) — just not for per-container write paths. - Qdrant data dir (
-
The LXC has its own 8 GiB swap. Combined with the host's 31 GiB, that's a lot of swap for guests that should never page. Drop the LXC swap allocation to 1–2 GiB (
pct set 104 -swap 2048) — frees disk on the LVM-thin pool and forces issues to surface earlier rather than silently swap. -
Stacks live in Dockge, not raw compose. The LXC is the Dockge flavor of helper-scripts. Deployment writes a stack named
nexain the Dockge UI; the compose YAML is stored under/opt/stacks/nexa/(Dockge default) and edited from the web UI. This means docs/09 "deploy" snippets translate to "paste this into Dockge → save → start", notdocker compose up -dover SSH. -
Untriaged local mail on the LXC. Console shows
You have new mail.at login — the system mail spool on/var/mail/roothas unread messages, almost always cron job failures.mailxormuttto inspect, then either fix the failing job or send the spool tonexa.systemntfy via a tiny aliases entry (root: |/usr/local/bin/spool-to-ntfy.sh). -
The LXC has its own 8 GiB swap. Combined with the host's 31 GiB, that's a lot of swap for guests that should never page. Drop the LXC swap allocation to 1–2 GiB (
pct set 104 -swap 2048) — frees disk on the LVM-thin pool and forces issues to surface earlier rather than silently swap.