# Arr Stack + Local AI A dockerized media automation stack (VPN-locked torrents + Jellyfin) **plus** a local AI stack built around [Ternary-Bonsai-27B](https://huggingface.co/prism-ml/Ternary-Bonsai-27B-gguf) for the **Beelink SER5 PRO** (Ryzen 7 7735HS + Radeon 680M). ## Services | Service | URL | Purpose | |-------------|------------------------|--------------------------------------| | **Landing** | http://host/ | Dark map of all LAN services (Caddy) | | Jellyfin | http://host:8096 | Media server / playback | | qBittorrent | http://host:8080 | Torrent client (VPN-only) | | Prowlarr | http://host:9696 | Indexer manager | | FlareSolverr| (internal :8191) | Cloudflare proxy for indexers | | Sonarr | http://host:8989 | TV shows | | Radarr | http://host:7878 | Movies | | Bazarr | http://host:6767 | Subtitles | | Lidarr | http://host:8686 | Music | | SABnzbd | http://host:8085 | Usenet NZB downloads | | **Forgejo** | http://host:3002 | Self-hosted Git (SSH :2222) | | **Open WebUI** | http://host:3000 | Chat UI, RAG, tools, code interpreter | | **Bonsai API** | http://host:8081/v1 | OpenAI-compatible llama-server | | SearXNG | http://127.0.0.1:8888 | Private web search (for RAG) | | OpenHands | http://host:3001 | Coding agent (`--profile coding`) | --- ## Media stack ### 1. Prepare ```bash cd arr-stack cp .env.example .env # then edit .env (PUID/PGID, TZ, LAN_SUBNET) ``` Drop your provider's WireGuard config into `wireguard/wg0.conf` and make the two edits described in `wireguard/wg0.conf.example`: 1. **Remove IPv6 addresses** from `Address` / `DNS` lines. 2. **Endpoint must be an IP**, not a hostname (`dig +short your.endpoint.host`). Create the data tree (already scaffolded here; on a fresh host run): ```bash mkdir -p config data/media/{movies,tv,music} data/torrents/{movies,tv,music} ``` The layout uses a **single `/data` share** — this lets the Arrs hardlink finished downloads into the media library instantly (no copy, no double disk usage, torrent keeps seeding): ``` data/ ├── media/ <- Jellyfin libraries │ ├── movies/ │ ├── tv/ │ └── music/ └── torrents/ <- qBittorrent downloads here ├── movies/ ├── tv/ └── music/ ``` ### 2. Start media ```bash docker compose up -d docker compose ps # gluetun must show (healthy) docker logs -f gluetun # wait for "You are running on the VPN" style line ``` qBittorrent waits for gluetun to be healthy before starting. ### 3. Verify the lockdown (do this once) ```bash # IP seen from inside qBittorrent — must be your VPN's IP, NOT your home IP: docker exec qbittorrent curl -s ifconfig.me && echo # Your real IP for comparison: curl -s ifconfig.me && echo # Killswitch test — kill the tunnel, traffic must die with it: docker exec gluetun sh -c "ip link del tun0" 2>/dev/null docker exec qbittorrent curl -s --max-time 8 ifconfig.me || echo "NO CONNECTIVITY (good)" docker restart gluetun # bring the tunnel back ``` Belt-and-suspenders extra: in qBittorrent → Tools → Options → Advanced, set **Network interface** to `tun0`. Even in a hypothetical gluetun failure, qBittorrent itself would then refuse to use any other interface. ### 4. Wire it together **qBittorrent** (http://host:8080 — default user `admin`, temporary password is printed in `docker logs qbittorrent` on first start; change it): - Options → Downloads → Default Save Path: `/data/torrents` - Create categories `movies` → `/data/torrents/movies` and `tv` → `/data/torrents/tv` **Prowlarr** (http://host:9696): - Add your indexers under *Indexers*. - *Settings → Apps*: add Sonarr (`http://sonarr:8989`), Radarr (`http://radarr:7878`), each with its API key (found in the Arr under *Settings → General*). Indexers sync automatically from then on. **Sonarr & Radarr** — *Settings → Download Clients* → add qBittorrent: - Host: `gluetun`, Port: `8080`, your qBittorrent credentials - Category: `tv` (Sonarr) / `movies` (Radarr) - *Settings → Media Management*: root folder `/data/media/tv` or `/data/media/movies`; **enable hardlinks** (on by default with this layout) **Bazarr**: point it at Sonarr/Radarr (`http://sonarr:8989` / `http://radarr:7878` + API keys); paths `/data/media/tv`, `/data/media/movies`. **Lidarr** (http://host:8686) — music, same patterns as Sonarr/Radarr: - *Settings → Download Clients* → qBittorrent: Host `gluetun`, Port `8080` - Category: `music` → qBittorrent save path `/data/torrents/music` - *Settings → Media Management*: root folder `/data/media/music`; hardlinks on - In **Prowlarr** → *Settings → Apps*: add Lidarr (`http://lidarr:8686` + API key) **Jellyfin** (http://host:8096): add libraries → Movies: `/data/media/movies`, Shows: `/data/media/tv`, Music: `/data/media/music`. Its media mount is read-only; enable trickplay/metadata into `/config` (the default) — it never writes to your media. ### 5. Optional: provider port forwarding If your VPN provider gives you a forwarded port (AirVPN, ProtonVPN, …), inbound peers can reach you and swarm connectivity improves: 1. Uncomment `FIREWALL_VPN_INPUT_PORTS` in the compose file and set the port. 2. For providers with a NAT-PMP API also uncomment `VPN_PORT_FORWARDING` and `VPN_PORT_FORWARDING_PROVIDER`. 3. Set the same port as qBittorrent's listening port (Options → Connection) and disable UPnP/NAT-PMP inside qBittorrent. Never publish qBittorrent's BitTorrent port on the host — inbound connections must arrive through the tunnel only. --- ## Local AI stack ### Why this design (and not plain Ollama) | Piece | Choice | Reason | |-------|--------|--------| | Model | **Ternary-Bonsai-27B** (~7.2 GB Q2_0) | ~95% of FP16 quality at laptop footprint; strong coding/math | | Runtime | **PrismML llama.cpp** (`llama-server`) | Custom Q2_0 g128 kernels — stock Ollama / mainline won't load the default GGUF | | Hardware | **SER5 PRO** Ryzen 7 7735HS + Radeon 680M | No NVIDIA → default is **CPU**; optional Vulkan for the iGPU | | Web UI | **Open WebUI** | Chat, document RAG, web search, tools, code interpreter | | Search | **SearXNG** | Keyless private search for RAG | | Coding | **OpenHands** (optional profile) + `/workspace` mount | Agent that edits files / runs commands in a sandbox | **Memory ballpark** (model alone, from Prism docs): ~8 GB at 4–10k context, ~14 GB at 100k with FP16 KV. With Arr + Jellyfin running, **32 GB system RAM is the practical minimum**; **64 GB** is comfortable for long context + media. ### 1. Download the model (~7.2 GB) ```bash # Recommended (resume-friendly): pip install -U "huggingface_hub[cli]" ./scripts/download-bonsai-model.sh # files land in ./models/bonsai/ ``` Optional: ```bash DOWNLOAD_DRAFTER=1 ./scripts/download-bonsai-model.sh # speculative decoding pack (CUDA-oriented) ``` ### 2. Start AI services ```bash # Build Prism llama-server image + start UI + search docker compose up -d --build bonsai open-webui searxng # Health curl -s http://localhost:8081/health curl -s http://localhost:8081/v1/models ``` Open **http://host:3000** — create the admin account on first visit. Open WebUI is pre-pointed at `http://bonsai:8080/v1` (see compose env). ### 3. RAG (documents & local files) **Upload / Knowledge collections (best for docs)** 1. Open WebUI → **Workspace → Knowledge** → create a collection. 2. Upload PDFs, markdown, code, notes. 3. In a chat, attach the collection (or enable it on the model). Embeddings download into the Open WebUI volume on first use (local, no cloud). **Whole folders via bind-mount** `AI_WORKSPACE_DIR` (default `./workspace`) is mounted **read-only** at `/workspace` inside Open WebUI. Put projects/notes there, or point `.env` at your real code tree: ```bash # .env AI_WORKSPACE_DIR=/home/you/src ``` Then ask e.g. “Summarize the layout of `/workspace/my-app`” after adding those paths to a Knowledge collection, or use tools/OpenHands for active editing. **Web RAG** SearXNG is wired as the search engine (`ENABLE_RAG_WEB_SEARCH=true`). In chat settings enable web search when you want live results. ### 4. Coding helpers | Tool | How | Best for | |------|-----|----------| | **Open WebUI + Knowledge** | Upload repo docs / mount `/workspace` | Q&A over code, design docs | | **Open WebUI code interpreter** | Enabled in compose | Python plots, quick analysis | | **OpenHands** | `docker compose --profile coding up -d openhands` | Multi-step “change the code / run tests” agents | | **Continue / Aider / Cursor** (host apps) | Base URL `http://host:8081/v1`, any API key | IDE-native completion & edits | **OpenHands** (http://host:3001): 1. Settings → enable Advanced. 2. Custom model: `openai/` (or keep the pre-set env). 3. Base URL: `http://bonsai:8080/v1` (from inside Docker) or `http://host.docker.internal:8081/v1`. 4. API key: same as `OPENAI_API_KEY` in `.env`. 5. Workspace is `${AI_WORKSPACE_DIR}` — agent sandboxes use Docker socket. > Note: Prism lists long-horizon multi-file agentic coding as a weaker area of > this 27B release; it is still very usable for review, refactors, and smaller > tasks. Prefer shorter goals and verify with tests. ### 5. Performance knobs (SER5 PRO) ```bash # .env examples # Leave headroom for Jellyfin/Arrs on a 16-thread 7735HS BONSAI_THREADS=12 # Longer context for big repos / PDFs (needs RAM) BONSAI_CTX=32768 BONSAI_KV4=1 # 4-bit KV cache if you approach OOM # Optional: Vulkan offload on Radeon 680M BONSAI_BACKEND=vulkan BONSAI_NGL=99 # then uncomment devices/group_add under bonsai in docker-compose.yml docker compose build --no-cache bonsai && docker compose up -d bonsai ``` Start with **CPU (`BONSAI_NGL=0`)**. iGPU Vulkan can help but shares system RAM and is more fiddly in Docker; measure both with a short prompt if you care. Recommended generation defaults (already set): `temp=0.7`, `top_p=0.95`, `top_k=20`. The 27B is a **thinking** model — answers may include a reasoning block; that is expected and improves quality. ### 6. Quick API smoke test ```bash curl -s http://localhost:8081/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-local-bonsai" \ -d '{ "model": "local", "messages": [{"role":"user","content":"Say hello in one short sentence."}], "temperature": 0.7, "max_tokens": 128 }' ``` (`model` can be any string or the id returned by `/v1/models` — llama-server is usually lenient.) ### 7. What not to do - Do **not** route bonsai/open-webui through gluetun — inference is local and the VPN killswitch would only add pain. - Do **not** expect stock Ollama to load `Ternary-Bonsai-27B-Q2_0.gguf` without Prism’s kernels (use our `bonsai` service). - Keep Open WebUI / OpenHands off the public internet; use LAN + auth (`WEBUI_AUTH=true`). --- ## Layout after AI setup ``` arr-stack/ ├── docker-compose.yml ├── .env / .env.example ├── ai/ # Prism llama-server image │ ├── Dockerfile │ └── entrypoint.sh ├── models/bonsai/ # GGUF weights (gitignored content) ├── workspace/ # files for RAG / OpenHands ├── searxng/settings.yml ├── scripts/download-bonsai-model.sh ├── config/ # app configs (open-webui, sonarr, …) ├── data/ # media + torrents └── wireguard/wg0.conf ``` ## Notes - All app-to-app traffic uses container DNS names (`gluetun`, `sonarr`, `bonsai`, …) on the internal `arr-net` bridge; only WebUIs are exposed to your LAN. - Update media stack with `docker compose pull && docker compose up -d`. Gluetun is pinned to major version `v3` — read its release notes before jumping majors. - Rebuild AI runtime after Prism release bumps: edit `PRISM_RELEASE` in `ai/Dockerfile`, then `docker compose build --no-cache bonsai`. - Optional media extras (commented in compose): Jellyseerr. - Optional coding profile: `docker compose --profile coding up -d openhands`.