114 lines
6.6 KiB
HTML
114 lines
6.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Homelab — plate-software.de</title>
|
|
<meta name="description" content="Wie alles auf *.plate-software.de aus einem TrueNAS zu Hause, einem winzigen VPS und einem IONOS-Reverse-Proxy ins Netz kommt." />
|
|
<meta name="author" content="Patrick Plate" />
|
|
<meta name="robots" content="index, follow" />
|
|
<meta property="og:title" content="Homelab — plate-software.de" />
|
|
<meta property="og:description" content="TrueNAS + frps + IONOS-Reverse-Proxy: wie aus „push to Gitea" in unter einer Minute öffentliches HTTPS wird." />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:locale" content="de_DE" />
|
|
<meta property="og:locale:alternate" content="en_US" />
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⬡</text></svg>" />
|
|
<link rel="stylesheet" href="/assets/style.css" />
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="wrap nav">
|
|
<a class="brand" href="/"><span class="mark">⬡</span> plate-software.de</a>
|
|
<nav class="nav-links">
|
|
<a href="/">
|
|
<span data-lang-de>Start</span>
|
|
<span data-lang-en>Home</span>
|
|
</a>
|
|
<a href="/apps/">Apps</a>
|
|
<a href="/homelab/" class="active">Homelab</a>
|
|
</nav>
|
|
<div class="lang-toggle" role="group" aria-label="Language">
|
|
<button id="btn-de" class="active" onclick="setLang('de')">DE</button>
|
|
<button id="btn-en" onclick="setLang('en')">EN</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="hero" style="padding: 56px 0 24px;">
|
|
<div class="wrap">
|
|
<a class="back-link" href="/">
|
|
<span data-lang-de>← Zurück</span>
|
|
<span data-lang-en>← Back</span>
|
|
</a>
|
|
<h1>
|
|
<span data-lang-de>Das <span class="grad">Homelab</span></span>
|
|
<span data-lang-en>The <span class="grad">Homelab</span></span>
|
|
</h1>
|
|
</div>
|
|
</section>
|
|
|
|
<section style="padding: 0 0 56px;">
|
|
<div class="wrap">
|
|
<div class="prose">
|
|
<p data-lang-de>Alles, was unter *.plate-software.de erreichbar ist, läuft auf einem TrueNAS bei mir zu Hause. Öffentlich wird das Ganze über einen winzigen VPS, der nichts anderes tut, als einen frps-Tunnel zu halten — davor steht ein IONOS-Reverse-Proxy, der TLS terminiert. Ein <code>git push</code> nach Gitea, der Runner baut das Docker-Image, und in unter einer Minute steht die neue Version öffentlich unter HTTPS.</p>
|
|
<p data-lang-en>Everything you can reach under *.plate-software.de runs on a TrueNAS at home. The whole thing is exposed publicly through a tiny VPS whose only job is to hold an frps tunnel — fronted by an IONOS reverse-proxy that terminates TLS. A <code>git push</code> to Gitea, the runner builds the Docker image, and in under a minute the new version is live on public HTTPS.</p>
|
|
|
|
<h3 data-lang-de>Der Stack</h3>
|
|
<h3 data-lang-en>The stack</h3>
|
|
<p data-lang-de>TrueNAS SCALE hostet Gitea, den act_runner und pro App einen Docker-Stack. Der VPS fährt ausschließlich frps (frp-Server) — kein Anwendungs-Code, keine Datenbank, einfach nur Tunnel-Endpunkt. Eingehender Traffic landet zuerst auf IONOS-Apache-vHosts, die per Reverse-Proxy in den frps-Tunnel zeigen. Let's-Encrypt-Zertifikate kommen via acme.sh.</p>
|
|
<p data-lang-en>TrueNAS SCALE hosts Gitea, the act_runner, and a per-app Docker stack. The VPS runs only frps (the frp server) — no application code, no database, just a tunnel endpoint. Public traffic enters IONOS Apache vhosts that reverse-proxy into the frps tunnel. Let's Encrypt certificates via acme.sh.</p>
|
|
|
|
<h3 data-lang-de>Warum so?</h3>
|
|
<h3 data-lang-en>Why this setup?</h3>
|
|
<p data-lang-de>Vor allem die Kosten: rund 10 €/Monat für VPS und Domain — bei einer vergleichbar gemanageten Hosting-Lösung wären es eher 80 €. Dazu volle Kontrolle, kein Vendor-Lock-in und ehrlich gesagt: Infrastruktur zu bauen ist die halbe Miete vom Spaß.</p>
|
|
<p data-lang-en>Mostly cost: about €10/month for VPS plus domain, versus around €80/month for an equivalent managed hosting setup. Add full control, no vendor lock-in, and frankly — building the infrastructure is half of why this is fun.</p>
|
|
|
|
<h3 data-lang-de>Was ich gelernt habe</h3>
|
|
<h3 data-lang-en>Lessons learned</h3>
|
|
<p data-lang-de>Zwei systemische Stolpersteine sind hängen geblieben: in einer Proxy-Kette muss <code>auth()</code> von NextAuth verwendet werden, <code>getToken()</code> bricht durch die mehrfachen Hops. Und die DNS-A-Records zeigen auf IONOS, nicht auf den VPS — wer das vergisst, wundert sich lange über kaputtes Routing.</p>
|
|
<p data-lang-en>Two systemic gotchas have stuck with me: in a proxy chain you need NextAuth's <code>auth()</code>, not <code>getToken()</code> — the latter breaks across the multiple hops. And the DNS A-records point to IONOS, not the VPS — forget that and you'll spend a long time wondering why routing is broken.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer>
|
|
<div class="wrap foot-row">
|
|
<div>© <span id="year"></span> Patrick Plate · plate-software.de</div>
|
|
<div class="foot-links">
|
|
<a href="/">
|
|
<span data-lang-de>Start</span>
|
|
<span data-lang-en>Home</span>
|
|
</a>
|
|
<a href="/apps/">Apps</a>
|
|
<a href="/homelab/">Homelab</a>
|
|
</div>
|
|
</div>
|
|
<div class="wrap">
|
|
<p class="deploy-badge" data-de="Letzter Deploy: <!--DEPLOY_INFO-->" data-en="Last deploy: <!--DEPLOY_INFO-->"></p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
function setLang(lang) {
|
|
document.documentElement.lang = lang;
|
|
document.getElementById('btn-de').classList.toggle('active', lang === 'de');
|
|
document.getElementById('btn-en').classList.toggle('active', lang === 'en');
|
|
document.querySelectorAll('[data-de][data-en]').forEach(function (el) {
|
|
el.textContent = el.getAttribute(lang === 'en' ? 'data-en' : 'data-de');
|
|
});
|
|
try { localStorage.setItem('ps-lang', lang); } catch (e) {}
|
|
}
|
|
(function () {
|
|
var saved;
|
|
try { saved = localStorage.getItem('ps-lang'); } catch (e) {}
|
|
var nav = (navigator.language || 'de').slice(0, 2);
|
|
setLang(saved || (nav === 'en' ? 'en' : 'de'));
|
|
var y = document.getElementById('year');
|
|
if (y) y.textContent = new Date().getFullYear();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|