stack/quickstart.md
tim c130080c52 Add Cloudflare DDNS for devopshomelab.com (IPv4 A records)
timothyjmiller/cloudflare-ddns with host networking; render env from
secrets (apex + subdomains mc/minecraft/vpn/www/forgejo/jellyfin).
2026-07-22 19:35:21 -07:00

250 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Quickstart
Homelab host: **`192.168.8.123`** (SER5, Ubuntu 24.04)
Stack: **`/opt/stack`** · Media: **`/storage`** · User: **`tim`**
---
## Fresh install / tech demo (max automation)
Almost everything lives in **`.env`** (see `.env.example`). **Every container deploys by default** (including OpenHands).
### Hardware profiles
| Profile | Hardware | Inference | Default model |
|---------|----------|-----------|----------------|
| **mini** | Beelink / SER5-class | **CPU** | Ternary-Bonsai-27B Q2_0 |
| **mid** | 8GB AMD GPU, 16GB RAM | **Vulkan** | Qwen3.5-9B Abliterated **Q4_K_M** (~5.6GB) |
| **gaming** | 16GB AMD GPU, 64GB RAM | **Vulkan** | Ternary-Bonsai-27B (+ downloads Qwen too) |
```bash
cp .env.example .env
# set STACK_HOST, LAN_SUBNET, usenet keys
# drop wireguard/wg0.conf (+ # EndpointHost = hostname)
./scripts/apply-profile.sh mini # or mid | gaming
./scripts/bootstrap-stack.sh
# → dirs, model download(s), build bonsai (cpu|vulkan), compose up ALL services
```
Switch later:
```bash
./scripts/apply-profile.sh mid
docker compose build bonsai && docker compose up -d
```
Demo login: **`tim` / `asdfasdf`**.
Still manual once: Prowlarr↔apps, Jellyfin libraries, first Forgejo/Open WebUI admin.
---
## Ansible
From a machine with this repo and SSH key `~/.ssh/id_ed25519`:
```bash
cd ansible
ansible-galaxy collection install -r requirements.yml
ansible-playbook site.yml -K # -K only if sudo still asks once; playbook installs NOPASSWD for tim
```
What it does: OS updates, tools (incl. Microsoft `edit`), Docker, AMD/Vulkan, UFW (LAN only), `/opt/stack` + `/storage`, `.env`, `arr-stack.service`, and **tim** as local admin (passwordless sudo, docker/video/render/…, owns stack + storage, permissive GPU nodes).
**Code on the server:** `/opt/stack` is a **git clone** of Forgejo `tim/stack` (`stack_deploy_method: git`).
Day-to-day updates on ser5:
```bash
ssh tim@192.168.8.123
cd /opt/stack
git pull
docker compose up -d # if compose/images changed
```
Ansible can also refresh code: `ansible-playbook site.yml -K --tags stack,deploy` (runs `git fetch` + checkout).
Local-only (not in git): `.env`, `config/`, `models/**/*.gguf`, `wireguard/wg0.conf`.
**Before first full stack start** (on ser5) — same as profile bootstrap:
```bash
ssh tim@192.168.8.123
cd /opt/stack
git pull
# wireguard/wg0.conf must exist (no IPv6; Endpoint = IP not hostname; # EndpointHost = …)
./scripts/apply-profile.sh mini # or mid | gaming
./scripts/bootstrap-stack.sh # models + build + compose up ALL services
# or step-by-step:
# ./scripts/download-models.sh
# docker compose build bonsai && docker compose up -d
sudo systemctl enable --now arr-stack # optional: start stack on boot
docker compose ps
```
**Compose GPU notes (mid / gaming)**
- Keep `devices`/`group_add` only in `docker-compose.override.yml` (not also in the main file).
- Use **numeric GIDs** for `group_add`, not names (`video`/`render` fail inside the image). Or omit `group_add` if `/dev/dri` is mode `0666`.
- **mini** uses CPU (`BONSAI_NGL=0`) — no GPU devices required.
Re-run playbook anytime: `ansible-playbook site.yml -K`
Tags: `--tags common,docker,amd,firewall,stack`
---
## Remotes
LAN URLs (UFW allows **`192.168.8.0/24`**). Replace host if needed.
| Service | URL |
|---------|-----|
| **Landing page** | http://192.168.8.123/ |
| Cloudflare DDNS | (background) apex + subdomains → public IPv4 |
| Jellyfin | http://192.168.8.123:8096 |
| qBittorrent | http://192.168.8.123:8080 |
| Prowlarr | http://192.168.8.123:9696 |
| Sonarr | http://192.168.8.123:8989 |
| Radarr | http://192.168.8.123:7878 |
| Bazarr | http://192.168.8.123:6767 |
| Lidarr | http://192.168.8.123:8686 |
| SABnzbd | http://192.168.8.123:8085 |
| Open WebUI | http://192.168.8.123:3000 |
| Bonsai API | http://192.168.8.123:8081/v1 |
| OpenHands | http://192.168.8.123:3001 |
| **Forgejo** | http://192.168.8.123:3002 |
| Forgejo Git SSH | `ssh://git@192.168.8.123:2222/user/repo.git` |
OpenHands is always started with the stack (GHCR images).
### Manual config (once)
**qBittorrent** — temp password in `docker logs qbittorrent`; user `admin`.
Advanced → Network interface: `tun0`.
Depends on **gluetun** healthy. If gluetun is unhealthy with `lookup … i/o timeout`, WireGuard is not passing traffic — usually a **stale Endpoint IP** in `wireguard/wg0.conf`.
**Auto-heal:** `scripts/gluetun-endpoint-watch.sh` + systemd timer every **5 minutes** (installed by Ansible). When gluetun is not healthy it: digs `WIREGUARD_ENDPOINT_HOST` → updates `Endpoint` in `wg0.conf` → recreates gluetun → recreates qbittorrent → tests exit IP.
Manual run: `cd /opt/stack && ./scripts/gluetun-endpoint-watch.sh`
Logs: `journalctl -u gluetun-endpoint-watch.service -n 50`
Set host in `.env`: `WIREGUARD_ENDPOINT_HOST=las-183-wg.whiskergalaxy.com`
Default save path + categories (WebUI):
1. **Tools → Options → Downloads**
- Default Save Path: `/data/torrents`
- Keep “Keep incomplete torrents in” off (or a subfolder under `/data/torrents` if you prefer).
2. **Categories** (same Options → Downloads page, or rightclick empty area in the left “Categories” panel → **Add category**):
| Category name | Save path |
|---------------|-----------|
| `movies` | `/data/torrents/movies` |
| `tv` | `/data/torrents/tv` |
| `music` | `/data/torrents/music` |
For each: **Add category** → name exactly `movies` / `tv` / `music` (lowercase, matches Sonarr/Radarr/Lidarr) → set **Save path** to the path above → OK.
3. Sonarr/Radarr/Lidarr download-client “Category” fields must match those names so torrents land in the right folder for hardlinks.
**Sonarr / Radarr / Lidarr** — Download client qBittorrent: host **`gluetun`**, port **`8080`**, categories `tv` / `movies` / `music`.
Root folders: `/data/media/tv`, `/data/media/movies`, `/data/media/music` (hardlinks on).
**Prowlarr** — Add indexers. Apps: Sonarr `http://sonarr:8989`, Radarr `http://radarr:7878`, Lidarr `http://lidarr:8686` + each API key (*Settings → General*).
**Cloudflare DDNS** (`timothyjmiller/cloudflare-ddns`) — updates **devopshomelab.com** and subdomains to your **public IPv4** every 5 minutes.
```bash
# secrets/.env must include CLOUDFLAREAPIKEY, CLOUDFLAREDOMAIN, CLOUDFLARESUBDOMAINS
./scripts/render-cloudflare-ddns-env.sh
docker compose up -d cloudflare-ddns
docker logs cloudflare-ddns --tail 30
```
Token needs **Zone → DNS → Edit**. `PROXIED=false` by default (DNS-only; better for VPN). Set `CLOUDFLARE_PROXIED=true` in secrets if you want orange-cloud for all names.
**Usenet (NZBgeek + Newshosting + Tweaknews)**
Credentials live in **`secrets/.env`** (not git). Example keys: see `secrets/.env.example`.
```bash
# on ser5
cd /opt/stack && git pull
mkdir -p /storage/usenet/{incomplete,complete/{movies,tv,music}}
# copy secrets if needed: scp secrets/.env tim@stack:/opt/stack/secrets/
docker compose up -d sabnzbd
# open http://192.168.8.123:8085 once if first-run wizard appears, then:
./scripts/configure-usenet.sh
```
1. **SABnzbd** (http://host:8085) — Config → Servers: Newshosting `news.newshosting.com:563` SSL; Tweaknews `news.tweaknews.eu:563` SSL. Test both green.
Folders: incomplete `/data/usenet/incomplete`, complete `/data/usenet/complete` (categories `movies`/`tv`/`music`).
2. **Prowlarr** → Indexers → **NZBgeek** → API key from secrets (`NZBKEEPKEY` / `NZBGEEK_API_KEY`).
Download Clients → **SABnzbd** → host `sabnzbd`, port `8080`, API key from SABnzbd Config → General.
Sync download client to apps (or add SABnzbd in each *arr).
3. **Sonarr/Radarr/Lidarr** → Download Clients → SABnzbd: host `sabnzbd`, port `8080`, categories `tv` / `movies` / `music`.
Usenet traffic is **not** forced through gluetun (SSL to provider). Torrents still go via VPN.
**FlareSolverr** (Cloudflare / JS challenges for some indexers) — container `flaresolverr` on `arr-net`, no public port by default.
Prowlarr → **Settings → Indexers** → FlareSolverr host: `http://flaresolverr:8191`.
**Bazarr** — Sonarr/Radarr URLs + API keys; paths `/data/media/tv`, `/data/media/movies`.
**Jellyfin** — Libraries: Movies `/data/media/movies`, Shows `/data/media/tv`, Music `/data/media/music`.
**Open WebUI** — Create admin on first visit. Model connection is pre-set to Bonsai (`http://bonsai:8080/v1`). Enable web search in chat if desired. RAG: Workspace → Knowledge (uploads) or files under `/workspace`.
**Bonsai** — No UI login. Smoke test: `curl -s http://192.168.8.123:8081/v1/models`. Needs GGUF in `models/bonsai/` and `docker compose build bonsai` once.
**OpenHands** — On the server (`/opt/stack`): `docker compose --profile coding up -d openhands`.
UI: http://192.168.8.123:3001
**LLM (required once)**
Settings → Advanced: base URL `http://bonsai:8080/v1`, API key `sk-local-bonsai`, model `openai/<id from /v1/models>`.
**Connect to Forgejo (pick A and/or B)**
**A — Provider (browse / open repos in OpenHands)**
1. Forgejo → avatar → **Settings → Applications** → generate a token (scopes: `read:repository`, `write:repository`, `read:user` — or full access on a trusted LAN).
2. OpenHands → **Settings → Git / Integrations** (or **Secrets** / provider list):
- Provider: **Forgejo** or **Gitea** (same API)
- Base URL: `http://forgejo:3000` (Docker DNS on `arr-net`; use this from OpenHands)
From a browser on your PC you use `http://192.168.8.123:3002` — that is *not* what the OpenHands container should use.
- Token: paste the Forgejo token
3. Start a conversation → choose repository → `tim/stack`.
**B — Workspace mount (most reliable for editing + git push)**
Compose already mounts `${AI_WORKSPACE_DIR}` (default `/opt/stack/workspace`) into the agent sandbox as `/workspace`.
On the **server**:
```bash
# HTTPS with token (one-time; token in URL — prefer SSH below)
# git clone http://tim:TOKEN@192.168.8.123:3002/tim/stack.git /opt/stack/workspace/stack
# SSH (key already on Forgejo for user tim)
git clone ssh://git@192.168.8.123:2222/tim/stack.git /opt/stack/workspace/stack
```
Then in OpenHands: work in `/workspace/stack` (or set the conversation workspace to that path).
Agent can `git commit` / `git push` if the host clone has credentials (SSH agent or stored remote URL).
If OpenHands cannot reach `forgejo` by name, ensure both services are on `arr-net` (`docker compose up -d forgejo openhands`).
**Forgejo** — http://192.168.8.123:3002
If connection refused: crash-loop from dual SSH; compose uses `START_SSH_SERVER=false`. Recreate: `docker compose up -d forgejo --force-recreate`.
---
## Locals
Bound to loopback on the host (not LAN-reachable as published):
| Service | URL / path |
|---------|------------|
| SearXNG | http://127.0.0.1:8888 |
Open WebUI talks to SearXNG on the Docker network (`http://searxng:8080`); you only need the loopback URL for direct browser checks on the server.
### Manual config
**SearXNG** — None for normal use. Settings: `/opt/stack/searxng/settings.yml`. Secret comes from `.env` (`SEARXNG_SECRET`).
**WireGuard** — File only: `/opt/stack/wireguard/wg0.conf` (not a web UI). Must be ready before gluetun is healthy.
**Host shell**`ssh tim@192.168.8.123` · stack logs: `docker compose -f /opt/stack/docker-compose.yml logs -f` · unit: `systemctl status arr-stack`.