From 3d4604c19ff43dfadb4fbbcdbeee7e6d0742175c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 19 Aug 2026 01:04:50 -0700 Subject: [PATCH] Stand up open-webui on dankclaw (quadlet, boot-persistent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the dead dankserver open-webui (bare container, :3000, pointed at a dead :9090 backend) with a boot-persistent quadlet on dankclaw: - CPU image (:main); dankclaw is CPU-only, so the :cuda image is wasted. - Isolated on slirp4netns; publishes 3005 (3000 is Gitea's HTTP port). - Default LLM backend is the local vLLM on 52625 (frank-default-large). - Data persists under /home/dank/Containers/open-webui/data. - Caddy route now proxies to the published LAN port (the container is not on Caddy's network). Also corrects the model/port mapping in the docs: the live state (verified 2026-08-19) is frank-default-large on vLLM :52625 (dankclaw) and frank-default-small on llama.cpp :7770 (dankserver) — the reverse of the 2026-08-17 note. --- dankclaw/Caddyfile | 4 +- .../open-webui/quadlet/open-webui.container | 32 ++++++ docs/Dankclaw/Dankclaw Stack.md | 23 ++-- docs/Dankclaw/OpenWebUI.md | 70 ++++++------ scripts/deploy-open-webui-quadlet.sh | 103 ++++++++++++++++++ tests/open_webui_quadlet.bats | 62 +++++++++++ 6 files changed, 250 insertions(+), 44 deletions(-) create mode 100644 dankclaw/open-webui/quadlet/open-webui.container create mode 100755 scripts/deploy-open-webui-quadlet.sh create mode 100644 tests/open_webui_quadlet.bats diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile index 9aa239b..d229daf 100644 --- a/dankclaw/Caddyfile +++ b/dankclaw/Caddyfile @@ -31,11 +31,13 @@ comfyui.dankclaw.local { } # --- OpenWebUI --- +# open-webui is a quadlet container on slirp4netns (isolated), so Caddy cannot +# reach it by container name. Proxy to its published LAN port instead. chat.dankclaw.local { basicauth { dank $2a$14$REPLACE_WITH_HASH } - reverse_proxy openwebui:8080 + reverse_proxy 192.168.0.143:3005 } # --- Hermes --- diff --git a/dankclaw/open-webui/quadlet/open-webui.container b/dankclaw/open-webui/quadlet/open-webui.container new file mode 100644 index 0000000..f11f6dd --- /dev/null +++ b/dankclaw/open-webui/quadlet/open-webui.container @@ -0,0 +1,32 @@ +# Quadlet unit: open-webui (rootless, boot-persistent). +# +# Install path on dankclaw: ~/.config/containers/systemd/open-webui.container +# Unit name: open-webui.service (container name: open-webui) +# +# Chat UI for the LLM fleet. It runs on the CPU image (dankclaw is CPU-only, so +# the :cuda image would waste a large pull for a GPU it cannot use) and points +# its OpenAI connection at the live vLLM backend on this host +# (host.containers.internal:52625, model frank-default-large). It is isolated on +# slirp4netns and publishes 3005 (3000 is Gitea's HTTP port). Data persists under +# /home/dank/Containers/open-webui/data. See docs/Dankclaw/OpenWebUI.md. + +[Unit] +Description=open-webui chat UI container +After=network.target + +[Container] +ContainerName=open-webui +Network=slirp4netns +Image=ghcr.io/open-webui/open-webui:main +PublishPort=3005:8080 +Environment=OPENAI_API_BASE_URL=http://host.containers.internal:52625 +Environment=ANONYMIZED_TELEMETRY=False +Environment=DO_NOT_TRACK=True +Volume=/home/dank/Containers/open-webui/data:/app/backend/data + +[Service] +TimeoutStartSec=900 +Restart=always + +[Install] +WantedBy=default.target diff --git a/docs/Dankclaw/Dankclaw Stack.md b/docs/Dankclaw/Dankclaw Stack.md index 27300a0..d2c6ae1 100644 --- a/docs/Dankclaw/Dankclaw Stack.md +++ b/docs/Dankclaw/Dankclaw Stack.md @@ -1,10 +1,10 @@ # Dankclaw Stack Dankclaw (192.168.0.143) runs the AI, search, and development stack. It -hosts the vLLM inference endpoint (`frank-default-small`, `:52625`, from +hosts the vLLM inference endpoint (`frank-default-large`, `:52625`, from the `vllmScripts` repo), the agent tools, the privacy search, and the -observability stack. The llama.cpp endpoint (`frank-default-large`, -`:7770`) runs on Dankserver (192.168.0.235) since the 2026-08-17 redo. +observability stack. The llama.cpp endpoint (`frank-default-small`, +`:7770`) runs on Dankserver (192.168.0.235). ```mermaid graph LR @@ -26,25 +26,26 @@ graph LR |---------|----------|------|------| | SearXNG | search.dankclaw.local | 8081 | [[SearXNG]] | | ComfyUI | comfyui.dankclaw.local | 8188 | [[ComfyUI]] | -| OpenWebUI | chat.dankclaw.local | 3000 | [[OpenWebUI]] | +| OpenWebUI | chat.dankclaw.local | 3005 | [[OpenWebUI]] | | Hermes | hermes.dankclaw.local | 8090 | [[Hermes]] | | Gitea | gitea.dankclaw.local | 3000 | [[Gitea]] | | Grafana | grafana.dankclaw.local | 3002 | [[Grafana]] | | Prometheus | prometheus.dankclaw.local | 9090 | [[Prometheus]] | | Homepage | dankclaw.local | 80/443 | — | -| frank-default-large (host service, Dankserver) | — | 7770 | [vllmScripts](../../vllmScripts/docs/index.md) | -| frank-default-small (host service) | — | 52625 | [vllmScripts](../../vllmScripts/docs/index.md) | +| frank-default-large (host service, Dankclaw) | — | 52625 | [vllmScripts](../../vllmScripts/docs/index.md) | +| frank-default-small (host service, Dankserver) | — | 7770 | [vllmScripts](../../vllmScripts/docs/index.md) | The last two rows are bare-metal systemd services, not containers. The -`vllmScripts` repository owns their scripts and knowledge base. Since the -2026-08-17 redo, `frank-default-large` runs llama.cpp FastMTP on -Dankserver; `frank-default-small` runs vLLM on Dankclaw. +`vllmScripts` repository owns their scripts and knowledge base. The live +mapping (verified 2026-08-19) is the reverse of the 2026-08-17 note: +`frank-default-large` runs vLLM on Dankclaw; `frank-default-small` runs +llama.cpp FastMTP on Dankserver. ## Boot Persistence -The Gitea trio and the SearXNG trio (gluetun, searxng, valkey) run as quadlet units, so they start at boot. User linger is on. ComfyUI runs as a systemd unit around a bare process. The remaining services in the table are defined in `compose.yaml` and are not yet boot-persistent. +The Gitea trio, the SearXNG trio (gluetun, searxng, valkey), and open-webui run as quadlet units, so they start at boot. User linger is on. ComfyUI runs as a systemd unit around a bare process. The remaining services in the table are defined in `compose.yaml` and are not yet boot-persistent. -Deploy the SearXNG trio with `scripts/deploy-searxng-quadlet.sh dankclaw`. The Gitea units install to `~/.config/containers/systemd/` (see [[Gitea]]). +Deploy the SearXNG trio with `scripts/deploy-searxng-quadlet.sh dankclaw` and open-webui with `scripts/deploy-open-webui-quadlet.sh dankclaw`. The Gitea units install to `~/.config/containers/systemd/` (see [[Gitea]]). ## Routing Rules diff --git a/docs/Dankclaw/OpenWebUI.md b/docs/Dankclaw/OpenWebUI.md index b702112..b2c1003 100644 --- a/docs/Dankclaw/OpenWebUI.md +++ b/docs/Dankclaw/OpenWebUI.md @@ -1,60 +1,66 @@ # OpenWebUI -OpenWebUI is a self-hosted chat interface for LLMs. It connects to OpenAI-compatible endpoints, Ollama, llama.cpp, and vLLM, and adds RAG, plugins, tool calling, and multi-user support. +OpenWebUI is a self-hosted chat interface for LLMs. It connects to OpenAI-compatible endpoints (vLLM, llama.cpp, Ollama) and adds RAG, plugins, tool calling, and multi-user support. -**Host:** Dankclaw — **Hostname:** `chat.dankclaw.local` — **Port:** 3000 — **Docs:** https://github.com/open-webui/open-webui +**Host:** Dankclaw — **Port:** 3005 (host) → 8080 (container) — **Boot-persistent:** Quadlet — **Docs:** https://github.com/open-webui/open-webui -## Facts (from research, primary source) +## Facts | Item | Value | |------|-------| -| Image | `ghcr.io/open-webui/open-webui:main` (CPU build for non-NVIDIA hosts) | -| Port | 3000 (host) → 8080 (container) | -| Data volume | config, sessions, RAG indexes | -| Backend | OpenAI-compatible endpoint at `http://192.168.0.235:7770/v1` — llama.cpp FastMTP on Dankserver (`frank-default-large`, Qwen3.8-27B IQ4_XS, 128K ctx) | +| Image | `ghcr.io/open-webui/open-webui:main` (CPU build; Dankclaw is CPU-only) | +| Port | 3005 (host) → 8080 (container). Port 3000 is Gitea's HTTP, so it is not usable. | +| Data volume | `/home/dank/Containers/open-webui/data` → `/app/backend/data` | +| Default backend | vLLM on Dankclaw `:52625` = `frank-default-large` (via `host.containers.internal:52625`) | +| Network | `slirp4netns` (isolated) | +| Persistence | Quadlet unit `open-webui.service`; `[Install] WantedBy=default.target` | +| Deploy | `scripts/deploy-open-webui-quadlet.sh dankclaw` | -> Note: the `:cuda` image tag was previously used on Dankserver. Dankclaw is AMD; use the default (CPU) image, NOT the cuda tag. +## The LLM backends -## Connecting to llama.cpp +The fleet runs two OpenAI-compatible endpoints. This mapping is the **live** state (verified 2026-08-19); it is the reverse of the 2026-08-17 note, which is now out of date. -OpenWebUI talks to the llama.cpp server on Dankserver through its OpenAI-compatible API. Configure a new connection in the UI or set the base URL: +| Endpoint | Model name | Host | Capability | +|----------|-----------|------|-----------| +| `:52625` (vLLM) | `frank-default-large` | Dankclaw | completion | +| `:7770` (llama.cpp FastMTP) | `frank-default-small` | Dankserver | completion, multimodal | -```yaml -environment: - OPENAI_API_BASE_URL: "http://192.168.0.235:7770/v1" - OPENAI_API_KEY: "none" -``` +The default connection points at the local vLLM (`frank-default-large`), so chat stays on-host. To add the other backend, open **Admin → Connections** in the UI and point an OpenAI connection at `http://192.168.0.235:7770/v1` (key `none`). -The endpoint moved from Dankclaw to Dankserver in the 2026-08-17 redo; use the LAN address, not `host.containers.internal` (which only reaches the podman host). +> Both endpoints are bare-metal systemd services managed by the sibling `vllmScripts` repository (not this repo). Model swaps, context sizing, and spec tuning happen there. See the [vllmScripts knowledge base](../../vllmScripts/docs/index.md). -> The `:7770` endpoint is the `frank-default-large` service on Dankserver, -> managed by the sibling `vllmScripts` repository (not this repo). Model -> swaps, context sizing, and spec tuning happen there. See the -> [vllmScripts knowledge base](../../vllmScripts/docs/index.md). -> -> Alternative backend: `frank-default-small` on port 52625 (vLLM on -> Dankclaw, nameistoken Qwen3.6-35B-A3B Quark W8A8 INT8, 262K ctx, -> tool calling). Same OpenAI-compatible API shape. Point -> `OPENAI_API_BASE_URL` at `http://192.168.0.143:52625/v1`. +## Boot persistence (Quadlet) + +The unit lives at `dankclaw/open-webui/quadlet/open-webui.container`. On the host it installs to `~/.config/containers/systemd/open-webui.container`. The generator turns it into `open-webui.service`. Because the unit sets `[Install] WantedBy=default.target` and user linger is on, it starts at boot. `systemctl --user is-enabled open-webui.service` reports `generated` (the quadlet boot-start state). ## Volumes -| Container path | Host path (convention) | -|----------------|------------------------| -| `/app/backend/data` | `/opt/openwebui/data` | +| Container path | Host path | +|----------------|-----------| +| `/app/backend/data` | `/home/dank/Containers/open-webui/data` | + +The data dir holds the database, RAG indexes, and uploads. The deploy script creates it if missing and preserves it across reruns. ## Healthcheck -OpenWebUI responds on `/health` with 200. +OpenWebUI responds on `/health` with 200. Probe it at `http://dankclaw:3005/health`. + +## Retired instances + +- The compose `open-webui` service (port 3000) was never run: 3000 is Gitea's HTTP port, so it could not bind. +- The bare-container `open-webui` on Dankserver (port 3000) pointed at a dead backend (`:9090`) and was removed. Its data dir is preserved on Dankserver in case it is needed. +- The bare-container `searxng` on Dankserver (port 8081) was removed. SearXNG now runs only on Dankclaw behind Caddy. ## Gotchas -- Use the non-CUDA image on AMD hosts. +- Use the non-CUDA image on this CPU-only host. - The default admin account is created on first login; secure it immediately. -- If the backend (llama.cpp) is slow to load, OpenWebUI may show connection errors until the model is ready — that matches the known 3–8 minute model load time. +- If the backend is loading (3–8 minute model load), OpenWebUI shows connection errors until the model is ready. +- `host.containers.internal` reaches only the podman host. To reach the Dankserver backend, use the LAN address `192.168.0.235:7770`. ## See Also - [[Dankclaw Stack]] - [[Hermes]] — the agent OpenWebUI can talk to -- [vllmScripts knowledge base](../../vllmScripts/docs/index.md) — the llama.cpp FastMTP and vLLM endpoints (Dankserver :7770, Dankclaw :52625) +- [[SearXNG]] — the search engine on this host +- [vllmScripts knowledge base](../../vllmScripts/docs/index.md) — the vLLM and llama.cpp endpoints diff --git a/scripts/deploy-open-webui-quadlet.sh b/scripts/deploy-open-webui-quadlet.sh new file mode 100755 index 0000000..794c6a3 --- /dev/null +++ b/scripts/deploy-open-webui-quadlet.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# deploy-open-webui-quadlet.sh — Install and (re)start open-webui on dankclaw. +# +# open-webui is boot-persistent via Quadlet (see AGENTS.md). This script: +# 1. Pushes main to origin +# 2. Pulls the repo on the target +# 3. Creates the data dir and copies the unit into ~/.config/containers/systemd/ +# 4. daemon-reloads and starts open-webui (first run pulls the image) +# +# It is idempotent: it stops any running unit and removes any container with the +# name (a prior run or a bare-container leftover) before reinstalling and +# starting. The data dir is preserved across reruns, so existing chats survive. +# +# Usage: +# ./scripts/deploy-open-webui-quadlet.sh [--dry-run] +# ./scripts/deploy-open-webui-quadlet.sh dankclaw +set -euo pipefail + +REMOTE_USER="dank" +REMOTE_HOST="dankclaw" +REMOTE_DIR="/home/dank/serverAppConfigs" +QDIR="dankclaw/open-webui/quadlet" +UNIT="open-webui.service" +DATA_DIR="/home/dank/Containers/open-webui/data" + +TARGET="${1:-}" +DRY_RUN=0 +if [ "${2:-}" = "--dry-run" ]; then + DRY_RUN=1 +fi + +if [ -z "${TARGET:-}" ]; then + echo "Usage: $0 [--dry-run]" + exit 1 +fi +if [ "${TARGET}" != "dankclaw" ]; then + echo "This stack runs only on dankclaw (got: ${TARGET})" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Local pre-flight +# --------------------------------------------------------------------------- +cd "$(dirname "$0")/.." +if ! git diff --quiet HEAD; then + echo "FAIL: Local working tree has uncommitted changes. Commit first." + exit 1 +fi + +if [ "$DRY_RUN" = "1" ]; then + echo "[dry-run] Would push main, pull on ${REMOTE_HOST}, install ${UNIT}, and start it (data at ${DATA_DIR})." + exit 0 +fi + +echo "=== Deploying open-webui quadlet to ${REMOTE_HOST} ===" +echo "[1/3] Pushing local main to origin..." +git push origin main + +REMOTE="${REMOTE_USER}@${REMOTE_HOST}" + +echo "[2/3] Installing and starting ${UNIT} on ${REMOTE_HOST}..." +ssh "$REMOTE" bash -s </dev/null || true + +echo ' Removing any container with the name (prior run or bare-container leftover)...' +podman rm -f open-webui 2>/dev/null || true + +echo ' Creating the data dir (preserved across reruns)...' +mkdir -p '${DATA_DIR}' + +echo ' Copying the quadlet unit into ~/.config/containers/systemd/ ...' +mkdir -p ~/.config/containers/systemd +cp ${QDIR}/open-webui.container ~/.config/containers/systemd/ + +echo ' daemon-reload (generates the unit)...' +# Quadlet units are generator-managed: systemctl enable fails on them. The +# [Install] WantedBy=default.target in the .container file is itself the +# enablement (is-enabled reports "generated"); with user linger on it starts at +# boot. So: daemon-reload, then start. +systemctl --user daemon-reload + +echo ' Starting (first run pulls the image; this can take a while)...' +systemctl --user start '${UNIT}' + +echo ' Unit state:' +systemctl --user --no-pager status '${UNIT}' 2>&1 | grep -E "●|Active:" || true +REMOTE_EOF + +echo "[3/3] Verifying open-webui is up..." +ssh "$REMOTE" ' + echo " is-enabled: $(systemctl --user is-enabled open-webui.service 2>&1) (boot-persistent)" + echo " port 3005:"; (ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -E ":3005\b" || echo " (not listening yet)" + echo " HTTP probe:"; curl -s -o /dev/null -w " http://127.0.0.1:3005 -> %{http_code}\n" -m 10 http://127.0.0.1:3005/ || echo " (no HTTP yet)" +' || true + +echo "=== open-webui quadlet deployed to ${REMOTE_HOST} ===" diff --git a/tests/open_webui_quadlet.bats b/tests/open_webui_quadlet.bats new file mode 100644 index 0000000..9df3f9f --- /dev/null +++ b/tests/open_webui_quadlet.bats @@ -0,0 +1,62 @@ +#!/usr/bin/env bats +# Seam: open-webui on dankclaw, boot-persistent via Quadlet. +# +# The old open-webui was a bare container on dankserver (port 3000) pointing at a +# dead LLM backend (:9090). This unit stands one up on dankclaw on a free port +# (3005), on the CPU image (dankclaw is CPU-only), wired to the live vLLM +# backend (host.containers.internal:52625, model frank-default-large). + +load helpers + +QDIR="$REPO_ROOT/dankclaw/open-webui/quadlet" + +@test "the open-webui quadlet file exists" { + [ -f "$QDIR/open-webui.container" ] +} + +@test "open-webui uses the CPU image, not :cuda (dankclaw is CPU-only)" { + grep -qE '^Image=ghcr\.io/open-webui/open-webui:main$' "$QDIR/open-webui.container" \ + || { echo "open-webui.container must use the :main (CPU) image"; return 1; } + if grep -qE '^Image=.*:cuda' "$QDIR/open-webui.container"; then + echo "open-webui.container must not use the :cuda image on a CPU-only host" + return 1 + fi + return 0 +} + +@test "open-webui is isolated on slirp4netns" { + grep -qE '^Network=slirp4netns' "$QDIR/open-webui.container" \ + || { echo "open-webui.container must set Network=slirp4netns"; return 1; } +} + +@test "open-webui publishes 3005:8080 (a free port; 3000 is Gitea)" { + grep -qE '^PublishPort=3005:8080' "$QDIR/open-webui.container" \ + || { echo "open-webui.container must publish 3005:8080"; return 1; } +} + +@test "open-webui points its LLM at the live vLLM backend (52625)" { + grep -qE '^Environment=OPENAI_API_BASE_URL=http://host\.containers\.internal:52625' \ + "$QDIR/open-webui.container" \ + || { echo "open-webui.container must set OPENAI_API_BASE_URL to the vLLM on 52625"; return 1; } +} + +@test "open-webui persists its data under /home/dank/Containers/open-webui/data" { + grep -qE '^Volume=/home/dank/Containers/open-webui/data:/app/backend/data' \ + "$QDIR/open-webui.container" \ + || { echo "open-webui.container must mount the data dir at /app/backend/data"; return 1; } +} + +@test "open-webui is installed for boot (WantedBy=default.target)" { + grep -qE '^WantedBy=default\.target' "$QDIR/open-webui.container" \ + || { echo "open-webui.container: [Install] must set WantedBy=default.target"; return 1; } +} + +@test "no secret material in the quadlet file" { + # The LLM backend is a local LAN endpoint; no key is required. No 44-char + # base64 secret-like value may appear in the tracked file. + if grep -qE '[A-Za-z0-9+/]{44}=' "$QDIR/open-webui.container"; then + echo "open-webui.container: contains a secret-like value" + return 1 + fi + has_no_leaked_secret "$QDIR/open-webui.container" +}