stack/specification.json
tim 1a6f915f04 Add paired specification.json and specification.html generator
Single SPEC source regenerates machine-readable JSON for agents and
dark HTML for humans so architecture survives lost chat sessions.
2026-07-22 19:54:42 -07:00

413 lines
13 KiB
JSON
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.

{
"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"
}
}