414 lines
13 KiB
JSON
414 lines
13 KiB
JSON
|
|
{
|
|||
|
|
"meta": {
|
|||
|
|
"name": "arr-stack",
|
|||
|
|
"title": "Homelab media + local AI stack",
|
|||
|
|
"version": "1.2.0",
|
|||
|
|
"generated_at": "2026-07-23T02:54:38Z",
|
|||
|
|
"generator": "scripts/generate-specification.py",
|
|||
|
|
"repository": {
|
|||
|
|
"forgejo": "http://192.168.8.123:3002/tim/stack",
|
|||
|
|
"clone_ssh": "ssh://git@192.168.8.123:2222/tim/stack.git",
|
|||
|
|
"default_branch": "main"
|
|||
|
|
},
|
|||
|
|
"primary_host": {
|
|||
|
|
"hostname": "stack",
|
|||
|
|
"lan_ip": "192.168.8.123",
|
|||
|
|
"user": "tim",
|
|||
|
|
"os": "Ubuntu Server 24.04",
|
|||
|
|
"hardware": "AMD Ryzen 7 5800H + Radeon Graphics (shared RAM), ~28–32 GB RAM",
|
|||
|
|
"stack_root": "/opt/stack",
|
|||
|
|
"media_root": "/storage"
|
|||
|
|
},
|
|||
|
|
"demo_credentials": {
|
|||
|
|
"username": "tim",
|
|||
|
|
"password": "asdfasdf",
|
|||
|
|
"note": "Tech demo only; used for local UIs when configured that way"
|
|||
|
|
},
|
|||
|
|
"docs_for_humans": [
|
|||
|
|
"specification.html",
|
|||
|
|
"quickstart.md",
|
|||
|
|
"README.md"
|
|||
|
|
],
|
|||
|
|
"docs_for_agents": [
|
|||
|
|
"specification.json",
|
|||
|
|
"AGENTS.md",
|
|||
|
|
"quickstart.md",
|
|||
|
|
"profiles/*.env",
|
|||
|
|
"docker-compose.yml"
|
|||
|
|
],
|
|||
|
|
"never_commit": [
|
|||
|
|
".env",
|
|||
|
|
"secrets/.env",
|
|||
|
|
"wireguard/wg0.conf",
|
|||
|
|
"config/",
|
|||
|
|
"models/**/*.gguf",
|
|||
|
|
"data/"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"architecture": {
|
|||
|
|
"summary": "Docker Compose stack combining VPN-locked torrents (*arr + qBittorrent via gluetun), Usenet (SABnzbd), media (Jellyfin), local LLM inference (Prism llama-server + Open WebUI), Forgejo git, OpenHands agent, Caddy LAN landing page, and Cloudflare DDNS.",
|
|||
|
|
"networks": {
|
|||
|
|
"arr-net": "Bridge network for most services",
|
|||
|
|
"gluetun_namespace": "qbittorrent uses network_mode: service:gluetun (VPN killswitch)",
|
|||
|
|
"host_network": [
|
|||
|
|
"cloudflare-ddns"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"data_layout": {
|
|||
|
|
"CONFIG_DIR": "App configs (default /opt/stack/config on server)",
|
|||
|
|
"DATA_DIR": "Shared media tree (default /storage on server)",
|
|||
|
|
"paths": {
|
|||
|
|
"media": "/data/media/{movies,tv,music}",
|
|||
|
|
"torrents": "/data/torrents/{movies,tv,music}",
|
|||
|
|
"usenet": "/data/usenet/incomplete and /data/usenet/complete/{movies,tv,music}",
|
|||
|
|
"models": "models/bonsai/*.gguf mounted read-only at /models in bonsai",
|
|||
|
|
"workspace": "AI_WORKSPACE_DIR for Open WebUI + OpenHands"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"security_model": {
|
|||
|
|
"torrents": "Only via WireGuard through gluetun; no BitTorrent port on host",
|
|||
|
|
"usenet": "Direct SSL to provider (not through gluetun)",
|
|||
|
|
"lan_firewall": "UFW allow service ports from LAN_SUBNET only",
|
|||
|
|
"landing_and_uis": "Plain HTTP on LAN (no TLS certs yet)"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"hardware_profiles": {
|
|||
|
|
"how_to_apply": "./scripts/apply-profile.sh mini|mid|gaming",
|
|||
|
|
"files": {
|
|||
|
|
"mini": "profiles/mini.env",
|
|||
|
|
"mid": "profiles/mid.env",
|
|||
|
|
"gaming": "profiles/gaming.env"
|
|||
|
|
},
|
|||
|
|
"profiles": {
|
|||
|
|
"mini": {
|
|||
|
|
"hardware": "Beelink / SER5-class mini PC",
|
|||
|
|
"inference": "cpu",
|
|||
|
|
"ngl": 0,
|
|||
|
|
"default_ctx": 32768,
|
|||
|
|
"threads": 12,
|
|||
|
|
"default_model": "Ternary-Bonsai-27B-Q2_0.gguf",
|
|||
|
|
"model_repo": "prism-ml/Ternary-Bonsai-27B-gguf",
|
|||
|
|
"notes": "CPU preferred on APU; do not force NGL in docker-compose.override.yml"
|
|||
|
|
},
|
|||
|
|
"mid": {
|
|||
|
|
"hardware": "~8GB AMD discrete GPU, ~16GB system RAM",
|
|||
|
|
"inference": "vulkan",
|
|||
|
|
"ngl": 99,
|
|||
|
|
"default_ctx": 8192,
|
|||
|
|
"threads": 8,
|
|||
|
|
"kv4": true,
|
|||
|
|
"default_model": "Qwen3.5-9B-abliterated-v2-MAX.Q4_K_M.gguf",
|
|||
|
|
"model_repo": "mradermacher/Qwen3.5-9B-abliterated-v2-MAX-GGUF",
|
|||
|
|
"notes": "Fits VRAM; use KV4 for memory"
|
|||
|
|
},
|
|||
|
|
"gaming": {
|
|||
|
|
"hardware": "~16GB AMD GPU, ~64GB system RAM",
|
|||
|
|
"inference": "vulkan",
|
|||
|
|
"ngl": 99,
|
|||
|
|
"default_ctx": 32768,
|
|||
|
|
"threads": 16,
|
|||
|
|
"default_model": "Ternary-Bonsai-27B-Q2_0.gguf",
|
|||
|
|
"also_download": [
|
|||
|
|
"Qwen3.5-9B Q4_K_M"
|
|||
|
|
],
|
|||
|
|
"notes": "Full Vulkan + large context"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"models": {
|
|||
|
|
"bonsai27": {
|
|||
|
|
"file": "Ternary-Bonsai-27B-Q2_0.gguf",
|
|||
|
|
"repo": "prism-ml/Ternary-Bonsai-27B-gguf",
|
|||
|
|
"runtime": "PrismML llama.cpp fork (custom Q2_0)",
|
|||
|
|
"size_approx": "~7.2 GB"
|
|||
|
|
},
|
|||
|
|
"qwen9_abliterated_q4km": {
|
|||
|
|
"file": "Qwen3.5-9B-abliterated-v2-MAX.Q4_K_M.gguf",
|
|||
|
|
"repo": "mradermacher/Qwen3.5-9B-abliterated-v2-MAX-GGUF",
|
|||
|
|
"runtime": "Same Prism llama-server image (standard GGUF)",
|
|||
|
|
"size_approx": "~5.6 GB"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"services": [
|
|||
|
|
{
|
|||
|
|
"id": "landing",
|
|||
|
|
"name": "Landing (Caddy)",
|
|||
|
|
"image": "caddy:2-alpine",
|
|||
|
|
"port": 80,
|
|||
|
|
"url": "http://HOST/",
|
|||
|
|
"purpose": "Dark-mode LAN resource map",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "gateway"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "gluetun",
|
|||
|
|
"name": "gluetun",
|
|||
|
|
"image": "qmcgaw/gluetun:v3",
|
|||
|
|
"port": null,
|
|||
|
|
"published_ports": [
|
|||
|
|
"8080:8080 (qBittorrent WebUI)"
|
|||
|
|
],
|
|||
|
|
"purpose": "WireGuard VPN + killswitch",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "vpn",
|
|||
|
|
"config": "wireguard/wg0.conf"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "qbittorrent",
|
|||
|
|
"name": "qBittorrent",
|
|||
|
|
"image": "lscr.io/linuxserver/qbittorrent:latest",
|
|||
|
|
"port": 8080,
|
|||
|
|
"url": "http://HOST:8080/",
|
|||
|
|
"purpose": "Torrent client (VPN-only via gluetun)",
|
|||
|
|
"network": "service:gluetun",
|
|||
|
|
"category": "download"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sabnzbd",
|
|||
|
|
"name": "SABnzbd",
|
|||
|
|
"image": "lscr.io/linuxserver/sabnzbd:latest",
|
|||
|
|
"port": 8085,
|
|||
|
|
"url": "http://HOST:8085/",
|
|||
|
|
"purpose": "Usenet NZB downloader",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "download"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "prowlarr",
|
|||
|
|
"name": "Prowlarr",
|
|||
|
|
"image": "lscr.io/linuxserver/prowlarr:latest",
|
|||
|
|
"port": 9696,
|
|||
|
|
"url": "http://HOST:9696/",
|
|||
|
|
"purpose": "Indexer manager (torrents + NZB)",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "download"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "flaresolverr",
|
|||
|
|
"name": "FlareSolverr",
|
|||
|
|
"image": "ghcr.io/flaresolverr/flaresolverr:latest",
|
|||
|
|
"port": 8191,
|
|||
|
|
"url": "http://flaresolverr:8191 (internal)",
|
|||
|
|
"purpose": "Cloudflare JS challenge proxy for Prowlarr",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "download",
|
|||
|
|
"published": false
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sonarr",
|
|||
|
|
"name": "Sonarr",
|
|||
|
|
"image": "lscr.io/linuxserver/sonarr:latest",
|
|||
|
|
"port": 8989,
|
|||
|
|
"url": "http://HOST:8989/",
|
|||
|
|
"purpose": "TV automation",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "media"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "radarr",
|
|||
|
|
"name": "Radarr",
|
|||
|
|
"image": "lscr.io/linuxserver/radarr:latest",
|
|||
|
|
"port": 7878,
|
|||
|
|
"url": "http://HOST:7878/",
|
|||
|
|
"purpose": "Movie automation",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "media"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "lidarr",
|
|||
|
|
"name": "Lidarr",
|
|||
|
|
"image": "lscr.io/linuxserver/lidarr:latest",
|
|||
|
|
"port": 8686,
|
|||
|
|
"url": "http://HOST:8686/",
|
|||
|
|
"purpose": "Music automation",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "media"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "bazarr",
|
|||
|
|
"name": "Bazarr",
|
|||
|
|
"image": "lscr.io/linuxserver/bazarr:latest",
|
|||
|
|
"port": 6767,
|
|||
|
|
"url": "http://HOST:6767/",
|
|||
|
|
"purpose": "Subtitles",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "media"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "jellyfin",
|
|||
|
|
"name": "Jellyfin",
|
|||
|
|
"image": "lscr.io/linuxserver/jellyfin:latest",
|
|||
|
|
"port": 8096,
|
|||
|
|
"url": "http://HOST:8096/",
|
|||
|
|
"purpose": "Media server / playback",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "media"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "bonsai",
|
|||
|
|
"name": "Bonsai (llama-server)",
|
|||
|
|
"image": "arr-stack/bonsai:local (ai/Dockerfile)",
|
|||
|
|
"port": 8081,
|
|||
|
|
"url": "http://HOST:8081/v1",
|
|||
|
|
"purpose": "Local LLM OpenAI-compatible API (Prism build)",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "ai"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "open-webui",
|
|||
|
|
"name": "Open WebUI",
|
|||
|
|
"image": "ghcr.io/open-webui/open-webui:main",
|
|||
|
|
"port": 3000,
|
|||
|
|
"url": "http://HOST:3000/",
|
|||
|
|
"purpose": "Chat UI + RAG + tools",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "ai",
|
|||
|
|
"llm_base": "http://bonsai:8080/v1"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "searxng",
|
|||
|
|
"name": "SearXNG",
|
|||
|
|
"image": "docker.io/searxng/searxng:latest",
|
|||
|
|
"port": 8888,
|
|||
|
|
"url": "http://127.0.0.1:8888/ (localhost only)",
|
|||
|
|
"purpose": "Private search for Open WebUI RAG",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "ai",
|
|||
|
|
"published": "loopback"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "openhands",
|
|||
|
|
"name": "OpenHands",
|
|||
|
|
"image": "ghcr.io/all-hands-ai/openhands:0.54",
|
|||
|
|
"port": 3001,
|
|||
|
|
"url": "http://HOST:3001/",
|
|||
|
|
"purpose": "AI coding agent (always deployed)",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "ai",
|
|||
|
|
"note": "No Forgejo integration in UI — clone repos into workspace"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "forgejo",
|
|||
|
|
"name": "Forgejo",
|
|||
|
|
"image": "codeberg.org/forgejo/forgejo:11",
|
|||
|
|
"port": 3002,
|
|||
|
|
"url": "http://HOST:3002/",
|
|||
|
|
"ssh_port": 2222,
|
|||
|
|
"purpose": "Self-hosted Git",
|
|||
|
|
"network": "arr-net",
|
|||
|
|
"category": "code"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "cloudflare-ddns",
|
|||
|
|
"name": "Cloudflare DDNS",
|
|||
|
|
"image": "timothyjmiller/cloudflare-ddns:latest",
|
|||
|
|
"port": null,
|
|||
|
|
"url": null,
|
|||
|
|
"purpose": "Update public IPv4 A records for domain + subdomains",
|
|||
|
|
"network": "host",
|
|||
|
|
"category": "dns",
|
|||
|
|
"domain": "devopshomelab.com",
|
|||
|
|
"subdomains": [
|
|||
|
|
"mc",
|
|||
|
|
"minecraft",
|
|||
|
|
"vpn",
|
|||
|
|
"www",
|
|||
|
|
"forgejo",
|
|||
|
|
"jellyfin"
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"scripts": {
|
|||
|
|
"generate-specification.py": "Regenerate specification.json + specification.html (this file)",
|
|||
|
|
"apply-profile.sh": "Merge profiles/{mini,mid,gaming}.env into .env",
|
|||
|
|
"bootstrap-stack.sh": "Fresh bring-up: dirs, models, compose up all, heal gluetun, usenet, cloudflare",
|
|||
|
|
"download-models.sh": "Download Bonsai 27B and/or Qwen 9B GGUF into models/bonsai",
|
|||
|
|
"download-bonsai-model.sh": "Wrapper → download-models.sh",
|
|||
|
|
"configure-usenet.sh": "SABnzbd folders + Newshosting/Tweaknews from secrets/.env",
|
|||
|
|
"render-cloudflare-ddns-env.sh": "Build config/cloudflare-ddns/ddns.env from secrets",
|
|||
|
|
"gluetun-endpoint-watch.sh": "If gluetun unhealthy: dig WG host, update Endpoint, recreate gluetun+qbit"
|
|||
|
|
},
|
|||
|
|
"ansible": {
|
|||
|
|
"entry": "ansible/site.yml",
|
|||
|
|
"inventory": "ansible/inventory/hosts.ini",
|
|||
|
|
"roles": [
|
|||
|
|
"common",
|
|||
|
|
"docker",
|
|||
|
|
"amd_vulkan",
|
|||
|
|
"firewall",
|
|||
|
|
"stack",
|
|||
|
|
"users"
|
|||
|
|
],
|
|||
|
|
"stack_deploy_method": "git (default) or rsync",
|
|||
|
|
"server_repo": "/opt/stack tracks Forgejo via git pull",
|
|||
|
|
"timers": [
|
|||
|
|
"arr-stack.service",
|
|||
|
|
"gluetun-endpoint-watch.timer"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"workflows": {
|
|||
|
|
"fresh_install": [
|
|||
|
|
"cp .env.example .env; fill STACK_HOST, LAN_SUBNET, usenet, cloudflare keys",
|
|||
|
|
"Place wireguard/wg0.conf (Endpoint IP + # EndpointHost = hostname)",
|
|||
|
|
"./scripts/apply-profile.sh mini|mid|gaming",
|
|||
|
|
"./scripts/bootstrap-stack.sh",
|
|||
|
|
"One-time UI: Prowlarr apps, Jellyfin libraries, Forgejo/Open WebUI admin (demo user/pass)"
|
|||
|
|
],
|
|||
|
|
"day_to_day_server": [
|
|||
|
|
"cd /opt/stack && git pull",
|
|||
|
|
"docker compose up -d",
|
|||
|
|
"rebuild bonsai if Dockerfile/profile backend changed"
|
|||
|
|
],
|
|||
|
|
"after_secrets_change": {
|
|||
|
|
"cloudflare": [
|
|||
|
|
"./scripts/render-cloudflare-ddns-env.sh",
|
|||
|
|
"docker compose up -d cloudflare-ddns --force-recreate"
|
|||
|
|
],
|
|||
|
|
"usenet_servers": [
|
|||
|
|
"./scripts/configure-usenet.sh",
|
|||
|
|
"Test servers in SABnzbd UI"
|
|||
|
|
],
|
|||
|
|
"nzbgeek": [
|
|||
|
|
"Update API key in Prowlarr UI"
|
|||
|
|
],
|
|||
|
|
"wireguard": [
|
|||
|
|
"Edit wireguard/wg0.conf",
|
|||
|
|
"docker compose up -d gluetun --force-recreate",
|
|||
|
|
"or ./scripts/gluetun-endpoint-watch.sh"
|
|||
|
|
],
|
|||
|
|
"stack_env": [
|
|||
|
|
"Edit .env",
|
|||
|
|
"docker compose up -d (+ build bonsai if backend changes)"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"switch_profile": [
|
|||
|
|
"./scripts/apply-profile.sh mid",
|
|||
|
|
"docker compose build bonsai && docker compose up -d"
|
|||
|
|
],
|
|||
|
|
"agent_git": [
|
|||
|
|
"Commit meaningful changes with complete sentences",
|
|||
|
|
"Push to origin main (Forgejo)",
|
|||
|
|
"Never commit secrets",
|
|||
|
|
"After architecture changes: run generate-specification.py and commit both outputs"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"compose": {
|
|||
|
|
"file": "docker-compose.yml",
|
|||
|
|
"override": "docker-compose.override.yml",
|
|||
|
|
"always_deploy_all": true,
|
|||
|
|
"note": "OpenHands is not behind a compose profile; full stack always starts",
|
|||
|
|
"override_rules": [
|
|||
|
|
"Do not put group_add names (video/render) — use host GIDs or 0666 /dev/dri",
|
|||
|
|
"Do not force bonsai NGL in override on mini (overrides .env)",
|
|||
|
|
"mini: jellyfin /dev/dri only; mid/gaming: add bonsai devices for Vulkan"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"env_and_secrets": {
|
|||
|
|
"stack_env": ".env (from .env.example) — ports, paths, AI, bootstrap flags, demo login",
|
|||
|
|
"secrets_env": "secrets/.env — usenet + cloudflare API (gitignored)",
|
|||
|
|
"approx_env_keys": 80,
|
|||
|
|
"demo_user": "tim",
|
|||
|
|
"demo_pass": "asdfasdf"
|
|||
|
|
}
|
|||
|
|
}
|