195 lines
12 KiB
HTML
195 lines
12 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|||
|
|
<title>Homelab media + local AI stack — specification</title>
|
|||
|
|
<meta name="color-scheme" content="dark" />
|
|||
|
|
<style>
|
|||
|
|
:root {
|
|||
|
|
--bg: #0c0e12; --elev: #141820; --card: #1a1f2a; --border: #2a3344;
|
|||
|
|
--text: #e8ecf4; --muted: #8b95a8; --accent: #6ea8fe; --ok: #7dd3a0;
|
|||
|
|
--font: "Segoe UI", system-ui, sans-serif;
|
|||
|
|
--mono: ui-monospace, "Cascadia Code", Menlo, monospace;
|
|||
|
|
}
|
|||
|
|
* { box-sizing: border-box; }
|
|||
|
|
body {
|
|||
|
|
margin: 0; font-family: var(--font); background: var(--bg); color: var(--text);
|
|||
|
|
line-height: 1.55; padding: 2rem 1.25rem 4rem;
|
|||
|
|
background-image: radial-gradient(ellipse 80% 40% at 50% -10%, rgba(110,168,254,.12), transparent);
|
|||
|
|
}
|
|||
|
|
.wrap { max-width: 980px; margin: 0 auto; }
|
|||
|
|
h1 { font-size: 1.75rem; letter-spacing: -0.02em; margin: 0 0 .35rem; }
|
|||
|
|
h2 { font-size: 1.2rem; margin: 2rem 0 .75rem; border-bottom: 1px solid var(--border); padding-bottom: .4rem; }
|
|||
|
|
h3 { font-size: 1.05rem; margin: 0 0 .4rem; }
|
|||
|
|
h4 { font-size: .95rem; margin: 1rem 0 .35rem; color: var(--accent); }
|
|||
|
|
p, li { color: var(--muted); }
|
|||
|
|
.lead { color: var(--text); font-size: 1.05rem; }
|
|||
|
|
.meta {
|
|||
|
|
display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0 1.5rem;
|
|||
|
|
}
|
|||
|
|
.pill {
|
|||
|
|
font-family: var(--mono); font-size: .78rem; padding: .3rem .65rem;
|
|||
|
|
border: 1px solid var(--border); border-radius: 999px; background: var(--elev); color: var(--accent);
|
|||
|
|
}
|
|||
|
|
.banner {
|
|||
|
|
background: var(--elev); border: 1px solid var(--border); border-radius: 12px;
|
|||
|
|
padding: 1rem 1.15rem; margin-bottom: 1.5rem;
|
|||
|
|
}
|
|||
|
|
.banner strong { color: var(--ok); }
|
|||
|
|
code { font-family: var(--mono); font-size: .88em; background: var(--card); padding: .1rem .35rem; border-radius: 4px; color: var(--accent); }
|
|||
|
|
a { color: var(--accent); }
|
|||
|
|
table { width: 100%; border-collapse: collapse; font-size: .88rem; margin: .75rem 0 1.25rem; }
|
|||
|
|
th, td { text-align: left; padding: .5rem .55rem; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|||
|
|
th { color: var(--text); font-weight: 600; background: var(--elev); }
|
|||
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: .75rem; }
|
|||
|
|
.card {
|
|||
|
|
background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 1rem;
|
|||
|
|
}
|
|||
|
|
.card ul { margin: .4rem 0 0; padding-left: 1.1rem; }
|
|||
|
|
.card li { margin: .25rem 0; }
|
|||
|
|
.muted { color: var(--muted); font-size: .9rem; }
|
|||
|
|
footer { margin-top: 2.5rem; font-size: .8rem; color: #5c667a; text-align: center; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="wrap">
|
|||
|
|
<h1>Homelab media + local AI stack</h1>
|
|||
|
|
<p class="lead">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.</p>
|
|||
|
|
<div class="meta">
|
|||
|
|
<span class="pill">spec v1.2.0</span>
|
|||
|
|
<span class="pill">generated 2026-07-23T02:54:38Z</span>
|
|||
|
|
<span class="pill">host 192.168.8.123</span>
|
|||
|
|
<span class="pill">stack /opt/stack</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="banner">
|
|||
|
|
<strong>Source of truth:</strong> edit <code>scripts/generate-specification.py</code> (the <code>SPEC</code> dict),
|
|||
|
|
then run <code>./scripts/generate-specification.py</code>. That rewrites both
|
|||
|
|
<code>specification.json</code> (agents) and <code>specification.html</code> (you) so they never drift.
|
|||
|
|
Agents should prefer <code>specification.json</code> + <code>AGENTS.md</code>; humans use this HTML and <code>quickstart.md</code>.
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<h2>Primary host</h2>
|
|||
|
|
<ul>
|
|||
|
|
<li>LAN: <code>http://192.168.8.123/</code> (Caddy landing)</li>
|
|||
|
|
<li>User: <code>tim</code> · OS: Ubuntu Server 24.04</li>
|
|||
|
|
<li>Hardware: AMD Ryzen 7 5800H + Radeon Graphics (shared RAM), ~28–32 GB RAM</li>
|
|||
|
|
<li>Git: <a href="http://192.168.8.123:3002/tim/stack">http://192.168.8.123:3002/tim/stack</a></li>
|
|||
|
|
<li>Demo login: <code>tim</code> / <code>asdfasdf</code></li>
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
<h2>Hardware profiles</h2>
|
|||
|
|
<p class="muted">Apply with <code>./scripts/apply-profile.sh mini|mid|gaming</code></p>
|
|||
|
|
<div class="grid">
|
|||
|
|
|
|||
|
|
<article class="card">
|
|||
|
|
<h3>mini</h3>
|
|||
|
|
<p class="muted">Beelink / SER5-class mini PC</p>
|
|||
|
|
<ul>
|
|||
|
|
<li><strong>Inference:</strong> cpu (ngl=0)</li>
|
|||
|
|
<li><strong>Context:</strong> 32768</li>
|
|||
|
|
<li><strong>Model:</strong> <code>Ternary-Bonsai-27B-Q2_0.gguf</code></li>
|
|||
|
|
<li>CPU preferred on APU; do not force NGL in docker-compose.override.yml</li>
|
|||
|
|
</ul>
|
|||
|
|
</article>
|
|||
|
|
|
|||
|
|
<article class="card">
|
|||
|
|
<h3>mid</h3>
|
|||
|
|
<p class="muted">~8GB AMD discrete GPU, ~16GB system RAM</p>
|
|||
|
|
<ul>
|
|||
|
|
<li><strong>Inference:</strong> vulkan (ngl=99)</li>
|
|||
|
|
<li><strong>Context:</strong> 8192</li>
|
|||
|
|
<li><strong>Model:</strong> <code>Qwen3.5-9B-abliterated-v2-MAX.Q4_K_M.gguf</code></li>
|
|||
|
|
<li>Fits VRAM; use KV4 for memory</li>
|
|||
|
|
</ul>
|
|||
|
|
</article>
|
|||
|
|
|
|||
|
|
<article class="card">
|
|||
|
|
<h3>gaming</h3>
|
|||
|
|
<p class="muted">~16GB AMD GPU, ~64GB system RAM</p>
|
|||
|
|
<ul>
|
|||
|
|
<li><strong>Inference:</strong> vulkan (ngl=99)</li>
|
|||
|
|
<li><strong>Context:</strong> 32768</li>
|
|||
|
|
<li><strong>Model:</strong> <code>Ternary-Bonsai-27B-Q2_0.gguf</code></li>
|
|||
|
|
<li>Full Vulkan + large context</li>
|
|||
|
|
</ul>
|
|||
|
|
</article>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<h2>Services</h2>
|
|||
|
|
<p class="muted">All services deploy by default (<code>docker compose up -d</code>). Replace HOST with 192.168.8.123. Links use plain <code>http://</code> (no TLS).</p>
|
|||
|
|
<table>
|
|||
|
|
<thead>
|
|||
|
|
<tr><th>ID</th><th>Name</th><th>Category</th><th>Port</th><th>URL</th><th>Purpose</th></tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr><td><code>landing</code></td><td>Landing (Caddy)</td><td>gateway</td><td>80</td><td><a href="http://192.168.8.123/">http://192.168.8.123/</a></td><td>Dark-mode LAN resource map</td></tr>
|
|||
|
|
<tr><td><code>gluetun</code></td><td>gluetun</td><td>vpn</td><td>—</td><td><a href="#">—</a></td><td>WireGuard VPN + killswitch</td></tr>
|
|||
|
|
<tr><td><code>qbittorrent</code></td><td>qBittorrent</td><td>download</td><td>8080</td><td><a href="http://192.168.8.123:8080/">http://192.168.8.123:8080/</a></td><td>Torrent client (VPN-only via gluetun)</td></tr>
|
|||
|
|
<tr><td><code>sabnzbd</code></td><td>SABnzbd</td><td>download</td><td>8085</td><td><a href="http://192.168.8.123:8085/">http://192.168.8.123:8085/</a></td><td>Usenet NZB downloader</td></tr>
|
|||
|
|
<tr><td><code>prowlarr</code></td><td>Prowlarr</td><td>download</td><td>9696</td><td><a href="http://192.168.8.123:9696/">http://192.168.8.123:9696/</a></td><td>Indexer manager (torrents + NZB)</td></tr>
|
|||
|
|
<tr><td><code>flaresolverr</code></td><td>FlareSolverr</td><td>download</td><td>8191</td><td><a href="http://flaresolverr:8191 (internal)">http://flaresolverr:8191 (internal)</a></td><td>Cloudflare JS challenge proxy for Prowlarr</td></tr>
|
|||
|
|
<tr><td><code>sonarr</code></td><td>Sonarr</td><td>media</td><td>8989</td><td><a href="http://192.168.8.123:8989/">http://192.168.8.123:8989/</a></td><td>TV automation</td></tr>
|
|||
|
|
<tr><td><code>radarr</code></td><td>Radarr</td><td>media</td><td>7878</td><td><a href="http://192.168.8.123:7878/">http://192.168.8.123:7878/</a></td><td>Movie automation</td></tr>
|
|||
|
|
<tr><td><code>lidarr</code></td><td>Lidarr</td><td>media</td><td>8686</td><td><a href="http://192.168.8.123:8686/">http://192.168.8.123:8686/</a></td><td>Music automation</td></tr>
|
|||
|
|
<tr><td><code>bazarr</code></td><td>Bazarr</td><td>media</td><td>6767</td><td><a href="http://192.168.8.123:6767/">http://192.168.8.123:6767/</a></td><td>Subtitles</td></tr>
|
|||
|
|
<tr><td><code>jellyfin</code></td><td>Jellyfin</td><td>media</td><td>8096</td><td><a href="http://192.168.8.123:8096/">http://192.168.8.123:8096/</a></td><td>Media server / playback</td></tr>
|
|||
|
|
<tr><td><code>bonsai</code></td><td>Bonsai (llama-server)</td><td>ai</td><td>8081</td><td><a href="http://192.168.8.123:8081/v1">http://192.168.8.123:8081/v1</a></td><td>Local LLM OpenAI-compatible API (Prism build)</td></tr>
|
|||
|
|
<tr><td><code>open-webui</code></td><td>Open WebUI</td><td>ai</td><td>3000</td><td><a href="http://192.168.8.123:3000/">http://192.168.8.123:3000/</a></td><td>Chat UI + RAG + tools</td></tr>
|
|||
|
|
<tr><td><code>searxng</code></td><td>SearXNG</td><td>ai</td><td>8888</td><td><a href="http://127.0.0.1:8888/ (localhost only)">http://127.0.0.1:8888/ (localhost only)</a></td><td>Private search for Open WebUI RAG</td></tr>
|
|||
|
|
<tr><td><code>openhands</code></td><td>OpenHands</td><td>ai</td><td>3001</td><td><a href="http://192.168.8.123:3001/">http://192.168.8.123:3001/</a></td><td>AI coding agent (always deployed)</td></tr>
|
|||
|
|
<tr><td><code>forgejo</code></td><td>Forgejo</td><td>code</td><td>3002</td><td><a href="http://192.168.8.123:3002/">http://192.168.8.123:3002/</a></td><td>Self-hosted Git</td></tr>
|
|||
|
|
<tr><td><code>cloudflare-ddns</code></td><td>Cloudflare DDNS</td><td>dns</td><td>—</td><td><a href="#">—</a></td><td>Update public IPv4 A records for domain + subdomains</td></tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
|
|||
|
|
<h2>Scripts</h2>
|
|||
|
|
<ul>
|
|||
|
|
<li><code>generate-specification.py</code> — Regenerate specification.json + specification.html (this file)</li>
|
|||
|
|
<li><code>apply-profile.sh</code> — Merge profiles/{mini,mid,gaming}.env into .env</li>
|
|||
|
|
<li><code>bootstrap-stack.sh</code> — Fresh bring-up: dirs, models, compose up all, heal gluetun, usenet, cloudflare</li>
|
|||
|
|
<li><code>download-models.sh</code> — Download Bonsai 27B and/or Qwen 9B GGUF into models/bonsai</li>
|
|||
|
|
<li><code>download-bonsai-model.sh</code> — Wrapper → download-models.sh</li>
|
|||
|
|
<li><code>configure-usenet.sh</code> — SABnzbd folders + Newshosting/Tweaknews from secrets/.env</li>
|
|||
|
|
<li><code>render-cloudflare-ddns-env.sh</code> — Build config/cloudflare-ddns/ddns.env from secrets</li>
|
|||
|
|
<li><code>gluetun-endpoint-watch.sh</code> — If gluetun unhealthy: dig WG host, update Endpoint, recreate gluetun+qbit</li>
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
<h2>After changing secrets</h2>
|
|||
|
|
<p class="muted">Edit <code>secrets/.env</code> first, then:</p>
|
|||
|
|
<h4>cloudflare</h4><ol><li><code>./scripts/render-cloudflare-ddns-env.sh</code></li><li><code>docker compose up -d cloudflare-ddns --force-recreate</code></li></ol>
|
|||
|
|
<h4>usenet_servers</h4><ol><li><code>./scripts/configure-usenet.sh</code></li><li><code>Test servers in SABnzbd UI</code></li></ol>
|
|||
|
|
<h4>nzbgeek</h4><ol><li><code>Update API key in Prowlarr UI</code></li></ol>
|
|||
|
|
<h4>wireguard</h4><ol><li><code>Edit wireguard/wg0.conf</code></li><li><code>docker compose up -d gluetun --force-recreate</code></li><li><code>or ./scripts/gluetun-endpoint-watch.sh</code></li></ol>
|
|||
|
|
<h4>stack_env</h4><ol><li><code>Edit .env</code></li><li><code>docker compose up -d (+ build bonsai if backend changes)</code></li></ol>
|
|||
|
|
|
|||
|
|
<h2>Fresh install</h2>
|
|||
|
|
<ol>
|
|||
|
|
<li><code>cp .env.example .env; fill STACK_HOST, LAN_SUBNET, usenet, cloudflare keys</code></li><li><code>Place wireguard/wg0.conf (Endpoint IP + # EndpointHost = hostname)</code></li><li><code>./scripts/apply-profile.sh mini|mid|gaming</code></li><li><code>./scripts/bootstrap-stack.sh</code></li><li><code>One-time UI: Prowlarr apps, Jellyfin libraries, Forgejo/Open WebUI admin (demo user/pass)</code></li>
|
|||
|
|
</ol>
|
|||
|
|
|
|||
|
|
<h2>Day-to-day on server</h2>
|
|||
|
|
<ol>
|
|||
|
|
<li><code>cd /opt/stack && git pull</code></li><li><code>docker compose up -d</code></li><li><code>rebuild bonsai if Dockerfile/profile backend changed</code></li>
|
|||
|
|
</ol>
|
|||
|
|
|
|||
|
|
<h2>Ansible</h2>
|
|||
|
|
<ul>
|
|||
|
|
<li>Entry: <code>ansible/site.yml</code></li>
|
|||
|
|
<li>Roles: <code>common</code>, <code>docker</code>, <code>amd_vulkan</code>, <code>firewall</code>, <code>stack</code>, <code>users</code></li>
|
|||
|
|
<li>Deploy method: git (default) or rsync</li>
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
<h2>Never commit</h2>
|
|||
|
|
<ul>
|
|||
|
|
<li><code>.env</code></li><li><code>secrets/.env</code></li><li><code>wireguard/wg0.conf</code></li><li><code>config/</code></li><li><code>models/**/*.gguf</code></li><li><code>data/</code></li>
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
<footer>
|
|||
|
|
Generated by scripts/generate-specification.py · paired with specification.json · arr-stack
|
|||
|
|
</footer>
|
|||
|
|
</div>
|
|||
|
|
</body>
|
|||
|
|
</html>
|