stack/specification.json

414 lines
13 KiB
JSON
Raw Normal View History

{
"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), ~2832 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"
}
}