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

11 KiB
Raw Blame History

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)
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:

./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:

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:

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:

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.

# 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.

# 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:

# 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).

Forgejohttp://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 shellssh tim@192.168.8.123 · stack logs: docker compose -f /opt/stack/docker-compose.yml logs -f · unit: systemctl status arr-stack.