Stand up open-webui on dankclaw (quadlet, boot-persistent)

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.
This commit is contained in:
Frank
2026-08-19 01:04:50 -07:00
parent 9f4c0d4175
commit 3d4604c19f
6 changed files with 250 additions and 44 deletions
+3 -1
View File
@@ -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 ---
@@ -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
+12 -11
View File
@@ -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
+38 -32
View File
@@ -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
+103
View File
@@ -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 <target> [--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 <target> [--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 <<REMOTE_EOF
set -euo pipefail
cd '${REMOTE_DIR}'
echo ' Pulling repo...'
git pull origin main
echo ' Stopping any running unit (idempotent)...'
systemctl --user stop '${UNIT}' 2>/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} ==="
+62
View File
@@ -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"
}