docs: rendered Nightjar infrastructure & cost report (HTML)
Some checks failed
ci / openapi (pull_request) Failing after 32s
ci / emulator (pull_request) Failing after 32s
ci / firmware (pull_request) Failing after 35s
ci / server (pull_request) Failing after 32s

Add docs/infrastructure-report.html - a standalone, self-contained HTML
render of the infrastructure and cost plan, a presentation-friendly
companion to docs/infrastructure.md.

Contents: cover + summary, the self-host topology on the MS-02 cluster
(Node A GPU AI worker, Node B app/API/Postgres/Redis, Node C MinIO
storage/HA) fronted by a Cloudflare Tunnel (no open ports, no static IP),
Hetzner cloud fallback, and a cost table (source/CI/docs already hosted at
zero marginal cost). Reflects the 2026-07-05 "self-host on the MS-02
cluster" decision (state/DECISIONS.md).

Inline CSS only, no external assets, GPL-3.0-only per the code licence.
No infrastructure was provisioned; this is a plan/inventory document.
This commit is contained in:
Laurence 2026-07-06 11:26:10 +01:00
parent f9bef7c30e
commit 43d98cffd0

View file

@ -0,0 +1,400 @@
<!doctype html>
<!-- SPDX-License-Identifier: GPL-3.0-only - Nightjar infrastructure report -->
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nightjar - Infrastructure & Cost Report</title>
<style>
:root{
--ink:#141a24; --muted:#5b6675; --line:#e2e7ee; --paper:#ffffff; --bg:#f4f6f9;
--amber:#e0894e; --amber-d:#c9722f; --teal:#2ba79d; --teal-l:#5fc7bf;
--navy:#13367a; --ok:#2f9e44; --soft:#f8fafc; --chip:#eef2f7;
--sans:"Segoe UI",system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;
--mono:"Cascadia Code",Consolas,ui-monospace,monospace;
}
*{box-sizing:border-box}
body{margin:0;font-family:var(--sans);color:var(--ink);background:var(--bg);line-height:1.55;font-size:15px}
.wrap{max-width:960px;margin:0 auto;padding:0 22px 80px}
header.cover{background:linear-gradient(150deg,#0e1116 0%,#1b2333 55%,#243a5e 100%);color:#eef3fa;border-radius:0 0 20px 20px;padding:54px 44px 46px;position:relative;overflow:hidden}
.cover .bird{position:absolute;right:26px;top:20px;opacity:.9}
.cover .kick{font-family:var(--mono);letter-spacing:2px;text-transform:uppercase;font-size:.72rem;color:var(--amber);margin:0 0 10px}
.cover h1{margin:0 0 8px;font-size:2.5rem;letter-spacing:-.5px}
.cover p.lede{margin:6px 0 0;max-width:600px;color:#c4d0e0;font-size:1.05rem}
.cover .meta{margin-top:22px;font-family:var(--mono);font-size:.76rem;color:#8ea2bd;display:flex;gap:20px;flex-wrap:wrap}
h2{font-size:1.5rem;margin:46px 0 6px;letter-spacing:-.3px}
h2 .n{color:var(--amber);font-family:var(--mono);font-size:1rem;margin-right:10px}
h3{font-size:1.08rem;margin:26px 0 8px}
.sub{color:var(--muted);margin:0 0 18px}
p{margin:10px 0}
.card{background:var(--paper);border:1px solid var(--line);border-radius:14px;padding:22px 24px;margin:16px 0;box-shadow:0 1px 2px rgba(20,26,36,.04)}
.tldr{background:linear-gradient(180deg,#fff,#fbfdff);border-left:4px solid var(--amber)}
.tldr strong{color:var(--amber-d)}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:16px 0}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:16px 0}
.phase{border-radius:14px;padding:18px 18px 16px;border:1px solid var(--line);background:var(--paper)}
.phase .tag{font-family:var(--mono);font-size:.7rem;text-transform:uppercase;letter-spacing:1px;padding:3px 9px;border-radius:999px;display:inline-block;margin-bottom:8px}
.p0 .tag{background:#e7f6ee;color:var(--ok)}
.p1 .tag{background:#fdf0e4;color:var(--amber-d)}
.p2 .tag{background:#e9eefb;color:var(--navy)}
.phase h4{margin:2px 0 4px;font-size:1.05rem}
.phase .cost{font-size:1.7rem;font-weight:800;letter-spacing:-.5px;margin:6px 0 2px}
.phase .cost small{font-size:.8rem;font-weight:600;color:var(--muted)}
.phase ul{margin:8px 0 0;padding-left:18px;color:#3a4453;font-size:.9rem}
.phase li{margin:3px 0}
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:.9rem}
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--line)}
th{background:var(--soft);font-size:.74rem;text-transform:uppercase;letter-spacing:.6px;color:var(--muted)}
td.num,th.num{text-align:right;font-family:var(--mono)}
tr:last-child td{border-bottom:none}
.total td{font-weight:800;background:#fbfdff;border-top:2px solid var(--line)}
.pill{font-family:var(--mono);font-size:.72rem;padding:2px 8px;border-radius:6px;background:var(--chip)}
.figcap{font-size:.8rem;color:var(--muted);text-align:center;margin:6px 0 0;font-style:italic}
.fig{margin:20px 0;background:var(--paper);border:1px solid var(--line);border-radius:14px;padding:18px}
.legend{display:flex;gap:16px;flex-wrap:wrap;justify-content:center;font-size:.78rem;color:var(--muted);margin-top:8px}
.legend span{display:inline-flex;align-items:center;gap:6px}
.dot{width:11px;height:11px;border-radius:3px;display:inline-block}
.note{font-size:.85rem;color:var(--muted)}
.rec{background:linear-gradient(160deg,#12331f,#0e1116);color:#e9f3ec;border:none}
.rec h2{color:#fff;margin-top:0}
.rec .n{color:var(--teal-l)}
.rec strong{color:var(--teal-l)}
.rec a{color:var(--teal-l)}
code{font-family:var(--mono);font-size:.85em;background:var(--chip);padding:1px 5px;border-radius:5px}
.foot{margin-top:40px;padding-top:18px;border-top:1px solid var(--line);font-size:.8rem;color:var(--muted)}
@media(max-width:720px){.grid3,.grid2{grid-template-columns:1fr}.cover h1{font-size:1.9rem}.cover .bird{display:none}}
@media print{body{background:#fff}.card,.fig{break-inside:avoid;box-shadow:none}header.cover{border-radius:0}}
</style>
</head>
<body>
<header class="cover">
<svg class="bird" width="120" height="120" viewBox="0 0 120 120" fill="none" aria-hidden="true">
<circle cx="60" cy="60" r="58" stroke="#e0894e" stroke-width="1.5" opacity=".35"/>
<path d="M30 78c14-4 20-14 22-26 8 10 18 12 30 10-6 8-4 18 4 26-16 4-30 2-42-4-6-3-12-6-14-12z" fill="#e0894e" opacity=".9"/>
<path d="M52 52c6-8 16-12 26-10-4 3-6 7-6 12" stroke="#5fc7bf" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<circle cx="66" cy="50" r="3" fill="#0e1116"/>
</svg>
<p class="kick">12 Hobbies Studio &nbsp;/&nbsp; Nightjar</p>
<h1>Infrastructure &amp; Cost Report</h1>
<p class="lede">Hosting the open-source project and the commercial service. Designed to start
on hardware you already own, at effectively zero cloud spend, and grow only when paying
users justify it.</p>
<div class="meta">
<span>PREPARED&nbsp;2026-07-03</span>
<span>SCOPE&nbsp;OSS&nbsp;+&nbsp;COMMERCIAL</span>
<span>STATUS&nbsp;PROPOSAL</span>
</div>
</header>
<div class="wrap">
<div class="card tldr">
<p style="margin-top:0"><strong>Bottom line.</strong> You do not need to spend anything on cloud
to launch. <strong>Phase&nbsp;0 runs entirely on your Proxmox / MS-02 cluster</strong> - app,
database, object storage and the GPU transcription worker - reachable from the public internet
through a free Cloudflare Tunnel. Your only cost is electricity, roughly
<strong>&pound;6-&pound;18/month</strong>. Hetzner does not enter the picture until you have real
users and want uptime that does not depend on your home connection - and even then the first
step is about <strong>&euro;13 (&asymp;&pound;11)/month</strong>.</p>
</div>
<h2><span class="n">01</span>The three phases at a glance</h2>
<p class="sub">Start really small. Each phase is a deliberate, reversible step - you only move up when the previous one hurts.</p>
<div class="grid3">
<div class="phase p0">
<span class="tag">Phase 0 - now</span>
<h4>All on your Proxmox</h4>
<div class="cost">&pound;6-18<small> /mo</small></div>
<div class="note">electricity only</div>
<ul>
<li>MS-02 cluster runs everything</li>
<li>Cloudflare Tunnel = free public URL</li>
<li>GPU node does transcription</li>
<li>Great for OSS demo + private beta</li>
</ul>
</div>
<div class="phase p1">
<span class="tag">Phase 1 - first users</span>
<h4>Proxmox + durable storage</h4>
<div class="cost">&asymp;&pound;11<small> /mo</small></div>
<div class="note">+ electricity + AI usage</div>
<ul>
<li>Compute stays on Proxmox</li>
<li>Customer audio -&gt; Hetzner Object Storage (off-site backup)</li>
<li>Commercial AI billed per minute</li>
<li>Still no always-on cloud server</li>
</ul>
</div>
<div class="phase p2">
<span class="tag">Phase 2 - paying scale</span>
<h4>Hybrid: cloud edge + home GPU</h4>
<div class="cost">&asymp;&pound;22-35<small> /mo</small></div>
<div class="note">+ AI usage, scales with revenue</div>
<ul>
<li>Always-on app node on Hetzner (uptime independent of home)</li>
<li>Private-tier GPU jobs still on MS-02</li>
<li>Object storage + managed backups</li>
<li>Add workers as load grows</li>
</ul>
</div>
</div>
<div class="fig">
<svg viewBox="0 0 900 150" width="100%" role="img" aria-label="Phase roadmap timeline">
<line x1="60" y1="75" x2="840" y2="75" stroke="#e2e7ee" stroke-width="4"/>
<line x1="60" y1="75" x2="300" y2="75" stroke="#2f9e44" stroke-width="4"/>
<!-- markers -->
<g>
<circle cx="120" cy="75" r="13" fill="#2f9e44"/>
<text x="120" y="80" text-anchor="middle" fill="#fff" font-size="12" font-family="monospace">0</text>
<text x="120" y="46" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">Launch</text>
<text x="120" y="108" text-anchor="middle" font-size="11" fill="#5b6675" font-family="sans-serif">Proxmox only</text>
<text x="120" y="124" text-anchor="middle" font-size="11" fill="#2f9e44" font-family="monospace">£6-18/mo</text>
</g>
<g>
<circle cx="400" cy="75" r="13" fill="#e0894e"/>
<text x="400" y="80" text-anchor="middle" fill="#fff" font-size="12" font-family="monospace">1</text>
<text x="400" y="46" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">First users</text>
<text x="400" y="108" text-anchor="middle" font-size="11" fill="#5b6675" font-family="sans-serif">+ off-site storage</text>
<text x="400" y="124" text-anchor="middle" font-size="11" fill="#c9722f" font-family="monospace">≈£11/mo</text>
</g>
<g>
<circle cx="680" cy="75" r="13" fill="#13367a"/>
<text x="680" y="80" text-anchor="middle" fill="#fff" font-size="12" font-family="monospace">2</text>
<text x="680" y="46" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">Paying scale</text>
<text x="680" y="108" text-anchor="middle" font-size="11" fill="#5b6675" font-family="sans-serif">cloud edge + home GPU</text>
<text x="680" y="124" text-anchor="middle" font-size="11" fill="#13367a" font-family="monospace">≈£22-35/mo</text>
</g>
<text x="840" y="70" text-anchor="end" font-size="11" fill="#9aa6b5" font-family="sans-serif">grows with revenue &#8594;</text>
</svg>
<p class="figcap">Figure 1 - You are here at Phase 0. Movement rightward is optional and demand-driven.</p>
</div>
<h2><span class="n">02</span>Phase 0 - everything on Proxmox (recommended start)</h2>
<p class="sub">One MS-02 node under Proxmox hosts the whole stack. The public reaches it through a Cloudflare
Tunnel, so there are no open ports and no static IP needed. This is enough to run the open-source
demo and onboard your first private beta testers.</p>
<div class="fig">
<svg viewBox="0 0 900 430" width="100%" role="img" aria-label="Phase 0 architecture on Proxmox">
<defs>
<marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
<path d="M0,0 L7,3 L0,6 Z" fill="#8ea2bd"/>
</marker>
</defs>
<!-- users -->
<rect x="30" y="180" width="140" height="70" rx="12" fill="#eef2f7" stroke="#cdd6e2"/>
<text x="100" y="210" text-anchor="middle" font-size="13" font-weight="700" font-family="sans-serif">Users</text>
<text x="100" y="230" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">web app + device</text>
<!-- cloudflare -->
<rect x="230" y="175" width="150" height="80" rx="12" fill="#fdf0e4" stroke="#e0894e"/>
<text x="305" y="205" text-anchor="middle" font-size="13" font-weight="700" fill="#c9722f" font-family="sans-serif">Cloudflare</text>
<text x="305" y="223" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">Tunnel + DNS + TLS</text>
<text x="305" y="239" text-anchor="middle" font-size="10" fill="#2f9e44" font-family="monospace">FREE</text>
<!-- proxmox host box -->
<rect x="440" y="40" width="430" height="360" rx="16" fill="#f8fafc" stroke="#13367a" stroke-width="1.5"/>
<text x="460" y="66" font-size="13" font-weight="800" fill="#13367a" font-family="sans-serif">MS-02 - Proxmox host (your hardware)</text>
<text x="852" y="66" text-anchor="end" font-size="10" fill="#9aa6b5" font-family="monospace">at home / office</text>
<!-- app vm -->
<rect x="462" y="86" width="180" height="92" rx="10" fill="#fff" stroke="#5fc7bf" stroke-width="1.5"/>
<text x="552" y="110" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">App VM / LXC</text>
<text x="552" y="129" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">FastAPI + web UI</text>
<text x="552" y="146" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">Postgres + Redis</text>
<text x="552" y="163" text-anchor="middle" font-size="10" fill="#2ba79d" font-family="monospace">2 vCPU / 4 GB</text>
<!-- storage -->
<rect x="662" y="86" width="188" height="92" rx="10" fill="#fff" stroke="#5fc7bf" stroke-width="1.5"/>
<text x="756" y="110" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">Object storage</text>
<text x="756" y="129" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">MinIO (S3 compatible)</text>
<text x="756" y="146" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">audio + transcripts</text>
<text x="756" y="163" text-anchor="middle" font-size="10" fill="#2ba79d" font-family="monospace">on local disk</text>
<!-- gpu worker -->
<rect x="462" y="196" width="388" height="98" rx="10" fill="#fff" stroke="#e0894e" stroke-width="1.5"/>
<text x="656" y="220" text-anchor="middle" font-size="12" font-weight="700" fill="#c9722f" font-family="sans-serif">GPU worker VM &#8212; RTX 3050 6GB (passthrough)</text>
<text x="656" y="241" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">faster-whisper transcription &#183; local LLM summaries (Ollama)</text>
<text x="656" y="261" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">this is your "Private tier" - data never leaves your hardware</text>
<text x="656" y="281" text-anchor="middle" font-size="10" fill="#c9722f" font-family="monospace">Hetzner has no GPU - this is why home GPU matters</text>
<!-- optional commercial ai -->
<rect x="462" y="312" width="388" height="66" rx="10" fill="#f4f6f9" stroke="#cdd6e2" stroke-dasharray="4 3"/>
<text x="656" y="336" text-anchor="middle" font-size="11.5" font-weight="700" font-family="sans-serif">Optional: commercial AI (Groq / OpenAI / Anthropic)</text>
<text x="656" y="356" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">for the "Cloud" tiers - pay per minute, no local GPU load</text>
<!-- arrows -->
<line x1="170" y1="215" x2="228" y2="215" stroke="#8ea2bd" stroke-width="2" marker-end="url(#arr)"/>
<line x1="380" y1="215" x2="460" y2="132" stroke="#8ea2bd" stroke-width="2" marker-end="url(#arr)"/>
<line x1="642" y1="150" x2="660" y2="150" stroke="#8ea2bd" stroke-width="2" marker-end="url(#arr)"/>
<line x1="552" y1="178" x2="600" y2="194" stroke="#8ea2bd" stroke-width="2" marker-end="url(#arr)"/>
</svg>
<p class="figcap">Figure 2 - Phase 0. One Proxmox host carries the app, database, storage and GPU worker.
Public access is via a free Cloudflare Tunnel - no ports opened, no fixed IP.</p>
</div>
<h3>What it costs</h3>
<table>
<thead><tr><th>Item</th><th>Detail</th><th class="num">&pound;/month</th></tr></thead>
<tbody>
<tr><td>Compute + storage + GPU</td><td>Runs on MS-02 you already own</td><td class="num">0</td></tr>
<tr><td>Public access</td><td>Cloudflare Tunnel + DNS + TLS (free tier)</td><td class="num">0</td></tr>
<tr><td>Source, CI, docs</td><td>Forgejo + marketing site already hosted</td><td class="num">0</td></tr>
<tr><td>Electricity - light use</td><td>1 node &asymp; 30 W avg @ &pound;0.27/kWh</td><td class="num">~6</td></tr>
<tr><td>Electricity - GPU busy</td><td>node + GPU under transcription load</td><td class="num">~12-18</td></tr>
<tr class="total"><td>Total</td><td>no cloud spend at all</td><td class="num">6-18</td></tr>
</tbody>
</table>
<p class="note">Commercial AI (if you offer the Cloud tiers) is billed per minute of audio and passed
through to the customer - it is a cost of goods, not fixed overhead. Groq transcription, for
example, is a fraction of a penny per minute.</p>
<h2><span class="n">03</span>Phase 1 - add off-site durability</h2>
<p class="sub">The moment real people trust you with recordings, a single home disk is no longer good
enough. The smallest responsible step is to replicate customer audio to <strong>Hetzner Object
Storage</strong> (S3-compatible, EU region for UK/GDPR). Everything else stays on Proxmox.</p>
<table>
<thead><tr><th>Item</th><th>Detail</th><th class="num">&euro;/mo</th><th class="num">&asymp;&pound;/mo</th></tr></thead>
<tbody>
<tr><td>Hetzner Object Storage</td><td>S3 bucket, EU, ~250 GB + egress</td><td class="num">~5</td><td class="num">~4.30</td></tr>
<tr><td>Compute / GPU / DB</td><td>still on Proxmox</td><td class="num">0</td><td class="num">0</td></tr>
<tr><td>Electricity</td><td>as Phase 0</td><td class="num">-</td><td class="num">6-18</td></tr>
<tr class="total"><td>New cloud spend</td><td>off-site backup of customer data</td><td class="num">~5</td><td class="num">~4-5</td></tr>
</tbody>
</table>
<p class="note">Cloudflare Tunnel still fronts the app - no always-on cloud server yet. If your home
connection drops, the service is down; that is acceptable for an early beta and is exactly what
Phase 2 fixes.</p>
<h2><span class="n">04</span>Phase 2 - hybrid, when uptime matters</h2>
<p class="sub">Once people pay, "down because my broadband blipped" is no longer OK. Move only the
<em>always-on, lightweight</em> parts to Hetzner - the app, database and queue - and keep the heavy
GPU work on your MS-02. Hetzner has no GPU, so the home cluster remains the Private-tier engine.</p>
<div class="fig">
<svg viewBox="0 0 900 380" width="100%" role="img" aria-label="Phase 2 hybrid architecture">
<defs>
<marker id="arr2" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
<path d="M0,0 L7,3 L0,6 Z" fill="#8ea2bd"/>
</marker>
</defs>
<!-- users -->
<rect x="24" y="165" width="120" height="60" rx="12" fill="#eef2f7" stroke="#cdd6e2"/>
<text x="84" y="190" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">Users</text>
<text x="84" y="208" text-anchor="middle" font-size="10" fill="#5b6675" font-family="sans-serif">+ devices</text>
<!-- hetzner cloud -->
<rect x="190" y="40" width="330" height="300" rx="16" fill="#f8fafc" stroke="#13367a" stroke-width="1.5"/>
<text x="210" y="66" font-size="13" font-weight="800" fill="#13367a" font-family="sans-serif">Hetzner Cloud - EU (Germany)</text>
<rect x="212" y="84" width="286" height="86" rx="10" fill="#fff" stroke="#5fc7bf" stroke-width="1.5"/>
<text x="355" y="108" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">App node - cax21 (ARM)</text>
<text x="355" y="127" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">FastAPI + Postgres + Redis + billing</text>
<text x="355" y="144" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">4 vCPU / 8 GB - always on</text>
<text x="355" y="161" text-anchor="middle" font-size="10" fill="#2ba79d" font-family="monospace">€12.59 / mo</text>
<rect x="212" y="184" width="286" height="70" rx="10" fill="#fff" stroke="#5fc7bf" stroke-width="1.5"/>
<text x="355" y="208" text-anchor="middle" font-size="12" font-weight="700" font-family="sans-serif">Object Storage (S3, EU)</text>
<text x="355" y="227" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">customer audio + backups</text>
<text x="355" y="244" text-anchor="middle" font-size="10" fill="#2ba79d" font-family="monospace">~€5 / mo + usage</text>
<rect x="212" y="268" width="286" height="58" rx="10" fill="#f4f6f9" stroke="#cdd6e2" stroke-dasharray="4 3"/>
<text x="355" y="290" text-anchor="middle" font-size="11.5" font-weight="700" font-family="sans-serif">Cloud tiers -&gt; commercial AI API</text>
<text x="355" y="308" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">Groq / OpenAI / Anthropic, per minute</text>
<!-- home -->
<rect x="560" y="90" width="315" height="200" rx="16" fill="#fdf7f0" stroke="#e0894e" stroke-width="1.5"/>
<text x="580" y="116" font-size="13" font-weight="800" fill="#c9722f" font-family="sans-serif">MS-02 - Proxmox (your hardware)</text>
<rect x="582" y="132" width="270" height="86" rx="10" fill="#fff" stroke="#e0894e" stroke-width="1.5"/>
<text x="717" y="156" text-anchor="middle" font-size="12" font-weight="700" fill="#c9722f" font-family="sans-serif">Private-tier GPU worker</text>
<text x="717" y="175" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">RTX 3050 - whisper + local LLM</text>
<text x="717" y="192" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">self-hosted, private data</text>
<text x="717" y="209" text-anchor="middle" font-size="10" fill="#c9722f" font-family="monospace">owned (electricity only)</text>
<text x="717" y="245" text-anchor="middle" font-size="10.5" fill="#5b6675" font-family="sans-serif">joined to app node over</text>
<text x="717" y="262" text-anchor="middle" font-size="11" font-weight="700" fill="#2ba79d" font-family="sans-serif">WireGuard / Tailscale mesh</text>
<!-- arrows -->
<line x1="144" y1="195" x2="188" y2="150" stroke="#8ea2bd" stroke-width="2" marker-end="url(#arr2)"/>
<line x1="498" y1="150" x2="580" y2="168" stroke="#2ba79d" stroke-width="2" stroke-dasharray="5 3" marker-end="url(#arr2)"/>
</svg>
<p class="figcap">Figure 3 - Phase 2. Cheap always-on ARM app node in Hetzner; heavy GPU work stays
home over an encrypted mesh. Cloud stays small; the expensive silicon is hardware you own.</p>
</div>
<table>
<thead><tr><th>Item</th><th>Spec</th><th class="num">&euro;/mo</th><th class="num">&asymp;&pound;/mo</th></tr></thead>
<tbody>
<tr><td>App node</td><td>cax21 ARM - 4 vCPU / 8 GB / 80 GB</td><td class="num">12.59</td><td class="num">~10.70</td></tr>
<tr><td>Object storage</td><td>S3, EU, + usage</td><td class="num">~5</td><td class="num">~4.30</td></tr>
<tr><td>Mesh to home GPU</td><td>Tailscale free / self-hosted WireGuard</td><td class="num">0</td><td class="num">0</td></tr>
<tr><td>Electricity (GPU)</td><td>MS-02 GPU node at home</td><td class="num">-</td><td class="num">6-18</td></tr>
<tr class="total"><td>Total</td><td>+ per-minute AI for Cloud tiers</td><td class="num">~18</td><td class="num">~22-35</td></tr>
</tbody>
</table>
<h2><span class="n">05</span>Hetzner options, for reference</h2>
<p class="sub">Real pricing pulled from your account catalogue (EU region, gross EUR incl. VAT). You do
not need any of these for Phase 0 - this is the menu for later. <strong>ARM (CAX) is the best value</strong>
and runs the whole stack.</p>
<table>
<thead><tr><th>Type</th><th>vCPU / RAM / disk</th><th>Good for</th><th class="num">&euro;/mo</th><th class="num">&asymp;&pound;/mo</th></tr></thead>
<tbody>
<tr><td>cax11 <span class="pill">ARM</span></td><td>2 / 4 GB / 40 GB</td><td>OSS demo box</td><td class="num">7.19</td><td class="num">6.10</td></tr>
<tr><td>cax21 <span class="pill">ARM</span></td><td>4 / 8 GB / 80 GB</td><td>app node (recommended)</td><td class="num">12.59</td><td class="num">10.70</td></tr>
<tr><td>cax31 <span class="pill">ARM</span></td><td>8 / 16 GB / 160 GB</td><td>app + workers at scale</td><td class="num">25.19</td><td class="num">21.40</td></tr>
<tr><td>cx23 <span class="pill">x86</span></td><td>2 / 4 GB / 40 GB</td><td>compatibility fallback</td><td class="num">6.59</td><td class="num">5.60</td></tr>
<tr><td>cpx31 <span class="pill">x86</span></td><td>4 / 8 GB / 160 GB</td><td>CPU-bound workers</td><td class="num">20.99</td><td class="num">17.85</td></tr>
<tr><td>ccx13 <span class="pill">dedicated</span></td><td>2 / 8 GB</td><td>guaranteed CPU</td><td class="num">51.59</td><td class="num">43.85</td></tr>
</tbody>
</table>
<p class="note"><strong>Important:</strong> Hetzner Cloud has <strong>no GPU instances</strong>. Any
self-hosted-model (Private tier) work must run on your MS-02, or on Hetzner's separate dedicated
GPU servers (~&euro;200+/mo) - which is exactly why keeping the GPU at home is the smart, cheap play.</p>
<h2><span class="n">06</span>What you already have</h2>
<div class="grid2">
<div class="card">
<h3 style="margin-top:0">Your Hetzner account</h3>
<ul style="padding-left:18px;margin:6px 0;color:#3a4453;font-size:.9rem">
<li><strong>howler</strong> - cx33 (4/8/80): ApisCP host running the Nightjar marketing site. Leave it be.</li>
<li><strong>detritus</strong> - cx21 (2/4/40): utility box. Could host the OSS demo if free.</li>
<li>Internal network, 6 spare IPs, firewalls, SSH keys - ready when needed.</li>
</ul>
</div>
<div class="card">
<h3 style="margin-top:0">Your hardware</h3>
<ul style="padding-left:18px;margin:6px 0;color:#3a4453;font-size:.9rem">
<li><strong>3x Minisforum MS-02</strong> under Proxmox - the primary platform for Phases 0-1.</li>
<li>One node has an <strong>RTX 3050 6 GB</strong> - the Private-tier GPU engine.</li>
<li>Plenty for demo + early beta; the GPU handles self-hosted transcription and summaries.</li>
</ul>
</div>
</div>
<div class="card rec">
<h2><span class="n">07</span>Recommendation</h2>
<p><strong>Do nothing in the cloud yet.</strong> Stand up Phase 0 on one MS-02 node under Proxmox:
an app VM (FastAPI + Postgres + Redis), a MinIO bucket for audio, and a GPU worker VM with the
RTX 3050 passed through. Publish it through a free Cloudflare Tunnel at
<code>app.nightjar.12hobbies.studio</code>. Total new spend: <strong>&pound;0</strong>, plus a few
pounds of electricity.</p>
<p>Only when you have people relying on it do you add <strong>Hetzner Object Storage</strong>
(Phase 1, ~&pound;4-5/mo) for off-site durability, and only when uptime becomes a promise do you
add the <strong>&euro;12.59/mo ARM app node</strong> (Phase 2). The expensive part - the GPU -
never moves to the cloud, because you already own it and Hetzner cannot beat free.</p>
<p style="margin-bottom:0"><strong>Next step I can take:</strong> draft the Proxmox build kit -
a <code>docker-compose.yml</code> for the app + MinIO stack, the Cloudflare Tunnel config, and a
short runbook to bring Phase 0 up on one MS-02. Say the word.</p>
</div>
<p class="foot">Nightjar / 12 Hobbies Studio - infrastructure proposal, 2026-07-03. Figures are
indicative: Hetzner prices are gross EUR from the live catalogue; &pound; conversions at
&asymp;&euro;1 = &pound;0.85; electricity at &pound;0.27/kWh. Read-only Hetzner token used for
inventory only; no resources were provisioned. Rotate the token when convenient.</p>
</div>
</body>
</html>