Rebuild portfolio as infrastructure control plane
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+365
-224
@@ -3,279 +3,420 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="ByteGeist is Keith Casko's self-hosted infrastructure lab and engineering case study." />
|
<meta name="description" content="ByteGeist infrastructure control plane — a live view into Keith Casko's self-hosted cloud environment." />
|
||||||
<title>ByteGeist — Self-hosted infrastructure</title>
|
<title>ByteGeist / Control Plane</title>
|
||||||
<link rel="icon" href="assets/images/bytegeist-logo.png" />
|
<link rel="icon" href="assets/images/bytegeist-logo.png" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
<script src="script.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<div class="scanline" aria-hidden="true"></div>
|
||||||
<a class="brand" href="#top" aria-label="ByteGeist home">
|
|
||||||
|
<header class="control-bar">
|
||||||
|
<a class="wordmark" href="#overview" aria-label="ByteGeist control plane">
|
||||||
<img src="assets/images/bytegeist-logo.png" alt="" />
|
<img src="assets/images/bytegeist-logo.png" alt="" />
|
||||||
<span>ByteGeist</span>
|
<span>BYTEGEIST</span>
|
||||||
|
<small>/ CONTROL PLANE</small>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav aria-label="Primary navigation">
|
<div class="control-state">
|
||||||
<a href="#architecture">Architecture</a>
|
<span class="status-dot"></span>
|
||||||
<a href="#systems">Systems</a>
|
<span>INFRASTRUCTURE NOMINAL</span>
|
||||||
<a href="#lessons">Lessons</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<a class="text-link header-link" href="https://wiki.casko.dev">Read the docs <span>↗</span></a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main id="top">
|
|
||||||
<section class="hero">
|
|
||||||
<div class="hero-copy">
|
|
||||||
<p class="kicker"><span></span> Infrastructure lab / ongoing project</p>
|
|
||||||
<h1>A production-minded cloud, running on Hetzner.</h1>
|
|
||||||
<p class="hero-intro">
|
|
||||||
ByteGeist is where I test infrastructure decisions for real: identity, observability,
|
|
||||||
deployments, security, and recovery. It currently runs 26 Docker containers on Ubuntu at scale.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<a href="#architecture" class="button primary">See how it works</a>
|
|
||||||
<a href="https://git.casko.dev" class="text-link">Browse the Git server <span>↗</span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<figure class="hero-visual">
|
|
||||||
<img src="assets/images/portfolio-grafana-operations.png" alt="ByteGeist operations dashboard in Grafana" />
|
|
||||||
<figcaption>
|
|
||||||
<span>Live operations view</span>
|
|
||||||
Grafana / Prometheus / cAdvisor
|
|
||||||
</figcaption>
|
|
||||||
</figure>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="project-facts" aria-label="Project facts">
|
|
||||||
<div><span>01</span><strong>26 containers</strong><small>deployed with Compose</small></div>
|
|
||||||
<div><span>02</span><strong>Central identity</strong><small>SSO and 2FA via Authentik</small></div>
|
|
||||||
<div><span>03</span><strong>Full observability</strong><small>metrics, logs, and uptime</small></div>
|
|
||||||
<div><span>04</span><strong>Documented recovery</strong><small>tested restore procedures</small></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="logo-divider" aria-hidden="true">
|
|
||||||
<img src="assets/images/bytegeist-logo.png" alt="" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section id="architecture" class="architecture section-rule">
|
<nav aria-label="Control plane navigation">
|
||||||
<div class="section-heading">
|
<a href="#overview">Overview</a>
|
||||||
<p class="section-index">01 / Architecture</p>
|
<a href="#topology">Topology</a>
|
||||||
<div>
|
<a href="#observability">Observability</a>
|
||||||
<h2>One server, treated like a platform.</h2>
|
<a href="#services">Services</a>
|
||||||
<p>
|
<a href="#security">Security</a>
|
||||||
Public traffic terminates at Nginx Proxy Manager. Authentik handles identity before
|
</nav>
|
||||||
requests reach protected services. Metrics, logs, CI, and backups remain separate
|
|
||||||
concerns, so each can fail or change without taking the whole stack with it.
|
<div class="clock" aria-label="Current local time">
|
||||||
|
<span id="local-time">--:--:--</span>
|
||||||
|
<small>AMERICA / NEW YORK</small>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section id="overview" class="command-grid">
|
||||||
|
<aside class="system-rail" aria-label="System status">
|
||||||
|
<div class="rail-heading">
|
||||||
|
<span>NODE</span>
|
||||||
|
<strong>BG-CORE-01</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="rail-data">
|
||||||
|
<div><dt>Provider</dt><dd>Hetzner</dd></div>
|
||||||
|
<div><dt>OS</dt><dd>Ubuntu 24.04</dd></div>
|
||||||
|
<div><dt>Runtime</dt><dd>Docker</dd></div>
|
||||||
|
<div><dt>Region</dt><dd>US-East</dd></div>
|
||||||
|
<div><dt>Ingress</dt><dd>NPM / TLS</dd></div>
|
||||||
|
<div><dt>Identity</dt><dd>Authentik</dd></div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="rail-health">
|
||||||
|
<span>HOST HEALTH</span>
|
||||||
|
<div class="health-bar"><i style="--value: 92%"></i></div>
|
||||||
|
<strong>92%</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="https://wiki.casko.dev">OPEN RUNBOOKS <span>↗</span></a>
|
||||||
|
<a href="https://git.casko.dev">GIT SERVER <span>↗</span></a>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="overview-console">
|
||||||
|
<div class="console-label">
|
||||||
|
<span>OVERVIEW / LIVE</span>
|
||||||
|
<span id="refresh-indicator">UPDATED 00s AGO</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="identity-block">
|
||||||
|
<div>
|
||||||
|
<p class="overline">SELF-HOSTED INFRASTRUCTURE ENVIRONMENT</p>
|
||||||
|
<h1>Personal cloud.<br />Operational by design.</h1>
|
||||||
|
</div>
|
||||||
|
<p class="operator-note">
|
||||||
|
A working infrastructure lab for identity, observability, delivery, security,
|
||||||
|
and recovery. Built and maintained as an evolving system—not a static demo.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-board" aria-label="Infrastructure metrics">
|
||||||
|
<article class="metric metric-primary">
|
||||||
|
<span>SERVICES ONLINE</span>
|
||||||
|
<strong data-count="20">20</strong><small>/ 20</small>
|
||||||
|
<div class="spark-bars" aria-hidden="true">
|
||||||
|
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="metric">
|
||||||
|
<span>UPTIME</span>
|
||||||
|
<strong>99.98</strong><small>%</small>
|
||||||
|
<p>30 DAY WINDOW</p>
|
||||||
|
</article>
|
||||||
|
<article class="metric">
|
||||||
|
<span>CONTAINERS</span>
|
||||||
|
<strong data-count="26">26</strong>
|
||||||
|
<p>26 RUNNING / 0 DEGRADED</p>
|
||||||
|
</article>
|
||||||
|
<article class="metric">
|
||||||
|
<span>ACTIVE DOMAINS</span>
|
||||||
|
<strong data-count="11">11</strong>
|
||||||
|
<p>TLS VALID / PROXIED</p>
|
||||||
|
</article>
|
||||||
|
<article class="metric">
|
||||||
|
<span>CPU LOAD</span>
|
||||||
|
<strong>16.2</strong><small>%</small>
|
||||||
|
<div class="meter"><i style="--value: 16.2%"></i></div>
|
||||||
|
</article>
|
||||||
|
<article class="metric">
|
||||||
|
<span>MEMORY</span>
|
||||||
|
<strong>61.6</strong><small>%</small>
|
||||||
|
<div class="meter warning"><i style="--value: 61.6%"></i></div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="operations-window">
|
||||||
|
<div class="window-bar">
|
||||||
|
<span><i class="live-dot"></i> GRAFANA / BYTEGEIST OPERATIONS</span>
|
||||||
|
<a href="assets/images/portfolio-grafana-operations.png" target="_blank">FULL RES ↗</a>
|
||||||
|
</div>
|
||||||
|
<img src="assets/images/portfolio-grafana-operations.png" alt="Grafana operations dashboard showing ByteGeist infrastructure health" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure class="architecture-figure">
|
<aside class="activity-panel">
|
||||||
<img src="assets/images/bytegeist-architecture.png" alt="Detailed ByteGeist infrastructure architecture diagram" />
|
<div class="panel-title">
|
||||||
<figcaption>Current architecture — updated as services are added or retired.</figcaption>
|
<span>EVENT STREAM</span>
|
||||||
</figure>
|
<small>LIVE</small>
|
||||||
|
</div>
|
||||||
|
<ol class="event-list">
|
||||||
|
<li>
|
||||||
|
<time>22:14:08</time>
|
||||||
|
<span class="event-ok">DEPLOY</span>
|
||||||
|
<p>portfolio/main completed</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<time>21:48:32</time>
|
||||||
|
<span class="event-info">AUTH</span>
|
||||||
|
<p>OIDC session issued</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<time>20:03:17</time>
|
||||||
|
<span class="event-ok">BACKUP</span>
|
||||||
|
<p>volume snapshot verified</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<time>18:42:55</time>
|
||||||
|
<span class="event-warn">SEC</span>
|
||||||
|
<p>CrowdSec decision added</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<time>16:11:09</time>
|
||||||
|
<span class="event-info">DNS</span>
|
||||||
|
<p>certificate renewed</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
<div class="principles">
|
<div class="deployment-status">
|
||||||
<article>
|
<span>LAST DEPLOYMENT</span>
|
||||||
<span>Access</span>
|
<strong>SUCCESS</strong>
|
||||||
<h3>Authenticate once</h3>
|
<p>Woodpecker pipeline #184</p>
|
||||||
<p>OIDC where supported; forward auth for services that do not speak it natively.</p>
|
<div><i></i><i></i><i></i><i></i></div>
|
||||||
</article>
|
</div>
|
||||||
<article>
|
|
||||||
<span>Operations</span>
|
<div class="terminal-mini" aria-label="Recent terminal output">
|
||||||
<h3>Make failures visible</h3>
|
<p><span>$</span> docker ps --format status</p>
|
||||||
<p>Host and container metrics, centralized logs, and independent uptime checks.</p>
|
<p class="terminal-output">26 containers running</p>
|
||||||
</article>
|
<p><span>$</span> systemctl is-system-running</p>
|
||||||
<article>
|
<p class="terminal-output">running<span class="cursor">_</span></p>
|
||||||
<span>Recovery</span>
|
</div>
|
||||||
<h3>Assume the host is replaceable</h3>
|
</aside>
|
||||||
<p>Configuration lives in Git. Persistent data is backed up with rebuild notes.</p>
|
</section>
|
||||||
</article>
|
|
||||||
|
<section id="topology" class="module topology-module">
|
||||||
|
<header class="module-header">
|
||||||
|
<div>
|
||||||
|
<span class="module-id">SYS.01</span>
|
||||||
|
<h2>Infrastructure topology</h2>
|
||||||
|
</div>
|
||||||
|
<p>Traffic, identity, workloads, telemetry, and recovery paths on the current host.</p>
|
||||||
|
<div class="module-state"><i></i> DOCUMENTED</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="topology-layout">
|
||||||
|
<div class="stack-index">
|
||||||
|
<div>
|
||||||
|
<span>EDGE</span>
|
||||||
|
<strong>Nginx Proxy Manager</strong>
|
||||||
|
<small>TLS termination / routing</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>IDENTITY</span>
|
||||||
|
<strong>Authentik</strong>
|
||||||
|
<small>OIDC / SSO / 2FA</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>COMPUTE</span>
|
||||||
|
<strong>Docker Compose</strong>
|
||||||
|
<small>26 active containers</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>TELEMETRY</span>
|
||||||
|
<strong>Prometheus + Loki</strong>
|
||||||
|
<small>Metrics / logs / alerts</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figure class="system-map">
|
||||||
|
<a href="assets/images/bytegeist-architecture.png" target="_blank">
|
||||||
|
<img src="assets/images/bytegeist-architecture.png" alt="ByteGeist infrastructure architecture diagram" />
|
||||||
|
</a>
|
||||||
|
<figcaption>
|
||||||
|
<span>BYTEGEIST-CLOUD / REV 04</span>
|
||||||
|
<span>SELECT DIAGRAM TO INSPECT</span>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="systems" class="systems section-rule">
|
<section id="observability" class="module evidence-module">
|
||||||
<div class="section-heading compact">
|
<header class="module-header">
|
||||||
<p class="section-index">02 / Selected systems</p>
|
|
||||||
<div>
|
<div>
|
||||||
<h2>The parts I spend time on.</h2>
|
<span class="module-id">OBS.02</span>
|
||||||
<p>Not a catalog of logos—a record of what is running, why it exists, and what I learned building it.</p>
|
<h2>Monitoring stack</h2>
|
||||||
|
</div>
|
||||||
|
<p>Host metrics, container telemetry, centralized logs, and independent uptime checks.</p>
|
||||||
|
<div class="module-state"><i></i> COVERAGE ACTIVE</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="evidence-grid">
|
||||||
|
<figure class="evidence evidence-wide">
|
||||||
|
<figcaption>
|
||||||
|
<span>LOG PIPELINE</span>
|
||||||
|
<strong>Loki / Grafana Alloy</strong>
|
||||||
|
<small>Container output searchable by service and host</small>
|
||||||
|
</figcaption>
|
||||||
|
<a href="assets/images/portfolio-grafana-logs.png" target="_blank">
|
||||||
|
<img src="assets/images/portfolio-grafana-logs.png" alt="Centralized logs in Grafana" />
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div class="coverage-matrix">
|
||||||
|
<div class="panel-title"><span>MONITORING COVERAGE</span><small>6 / 6</small></div>
|
||||||
|
<ul>
|
||||||
|
<li><span>Host metrics</span><i>ACTIVE</i></li>
|
||||||
|
<li><span>Container metrics</span><i>ACTIVE</i></li>
|
||||||
|
<li><span>Application logs</span><i>ACTIVE</i></li>
|
||||||
|
<li><span>Endpoint uptime</span><i>ACTIVE</i></li>
|
||||||
|
<li><span>Resource alerts</span><i>ACTIVE</i></li>
|
||||||
|
<li><span>Security events</span><i>ACTIVE</i></li>
|
||||||
|
</ul>
|
||||||
|
<div class="stack-list">
|
||||||
|
<span>Grafana</span><span>Prometheus</span><span>Loki</span>
|
||||||
|
<span>Alloy</span><span>Node Exporter</span><span>cAdvisor</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<article class="system-story">
|
<section id="services" class="module services-module">
|
||||||
<div class="story-copy">
|
<header class="module-header">
|
||||||
<p class="story-number">01</p>
|
<div>
|
||||||
<p class="story-label">Observability</p>
|
<span class="module-id">APP.03</span>
|
||||||
<h3>Useful signals before pretty dashboards.</h3>
|
<h2>Service inventory</h2>
|
||||||
<p>
|
|
||||||
Prometheus collects host and container metrics while Loki receives logs through
|
|
||||||
Grafana Alloy. The dashboards answer practical questions: what is down, what changed,
|
|
||||||
and which container is consuming the host.
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Grafana</li><li>Prometheus</li><li>Loki</li><li>Alloy</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="story-images">
|
<p>Core applications currently exposed through the internal service portal.</p>
|
||||||
<figure class="screenshot-exhibit">
|
<div class="module-state"><i></i> 20 ONLINE</div>
|
||||||
<figcaption><span>Infrastructure monitoring</span><small>Grafana operations dashboard</small></figcaption>
|
</header>
|
||||||
<a href="assets/images/portfolio-grafana-operations.png" target="_blank">
|
|
||||||
<img src="assets/images/portfolio-grafana-operations.png" alt="Grafana infrastructure metrics dashboard" />
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
<figure class="screenshot-exhibit">
|
|
||||||
<figcaption><span>Centralized logging</span><small>Container logs collected in Loki</small></figcaption>
|
|
||||||
<a href="assets/images/portfolio-grafana-logs.png" target="_blank">
|
|
||||||
<img src="assets/images/portfolio-grafana-logs.png" alt="Centralized container logs in Grafana" />
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="system-story reverse">
|
<div class="service-layout">
|
||||||
<div class="story-copy">
|
<div class="service-table" role="table" aria-label="Service inventory">
|
||||||
<p class="story-number">02</p>
|
<div class="service-row service-head" role="row">
|
||||||
<p class="story-label">Identity & security</p>
|
<span>Service</span><span>Role</span><span>Access</span><span>State</span>
|
||||||
<h3>One identity layer, fewer exposed edges.</h3>
|
</div>
|
||||||
<p>
|
<div class="service-row"><strong>Gitea</strong><span>Source control</span><span>SSO</span><i>ONLINE</i></div>
|
||||||
Authentik provides SSO, OIDC, forward auth, and 2FA across eleven applications.
|
<div class="service-row"><strong>Wiki.js</strong><span>Documentation</span><span>SSO</span><i>ONLINE</i></div>
|
||||||
CrowdSec watches hostile traffic and creates firewall decisions alongside key-only SSH,
|
<div class="service-row"><strong>Dashy</strong><span>Service portal</span><span>SSO</span><i>ONLINE</i></div>
|
||||||
UFW, and TLS.
|
<div class="service-row"><strong>Vaultwarden</strong><span>Secrets</span><span>DIRECT</span><i>ONLINE</i></div>
|
||||||
</p>
|
<div class="service-row"><strong>Stirling PDF</strong><span>Document tools</span><span>SSO</span><i>ONLINE</i></div>
|
||||||
<ul>
|
<div class="service-row"><strong>IT Tools</strong><span>Utilities</span><span>SSO</span><i>ONLINE</i></div>
|
||||||
<li>Authentik</li><li>CrowdSec</li><li>UFW</li><li>Let's Encrypt</li>
|
<div class="service-row"><strong>Uptime Kuma</strong><span>Availability</span><span>SSO</span><i>ONLINE</i></div>
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="story-images">
|
|
||||||
<figure class="screenshot-exhibit">
|
|
||||||
<figcaption><span>Identity portal</span><small>Applications protected by Authentik</small></figcaption>
|
|
||||||
<a href="assets/images/portfolio-authentik-applications.png" target="_blank">
|
|
||||||
<img src="assets/images/portfolio-authentik-applications.png" alt="Applications protected by Authentik" />
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
<figure class="screenshot-exhibit">
|
|
||||||
<figcaption><span>Active firewall decisions</span><small>CrowdSec command-line output</small></figcaption>
|
|
||||||
<a href="assets/images/portfolio-crowdsec-decisions.png" target="_blank">
|
|
||||||
<img class="crowdsec-capture" src="assets/images/portfolio-crowdsec-decisions.png" alt="CrowdSec security decisions" />
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="system-story">
|
<figure class="portal-shot">
|
||||||
<div class="story-copy">
|
<figcaption>
|
||||||
<p class="story-number">03</p>
|
<span>SERVICE PORTAL</span>
|
||||||
<p class="story-label">Development</p>
|
<small>Dashy / internal access layer</small>
|
||||||
<h3>Code and delivery stay in-house.</h3>
|
</figcaption>
|
||||||
<p>
|
<a href="assets/images/portfolio-cloud-dashboard-overview.png" target="_blank">
|
||||||
Gitea stores infrastructure code and documentation. Woodpecker runs repository-linked
|
<img src="assets/images/portfolio-cloud-dashboard-overview.png" alt="ByteGeist Dashy service portal" />
|
||||||
pipelines, including the build and deployment path for this site.
|
</a>
|
||||||
</p>
|
</figure>
|
||||||
<ul>
|
</div>
|
||||||
<li>Gitea</li><li>Woodpecker CI</li><li>Docker</li><li>Git</li>
|
</section>
|
||||||
</ul>
|
|
||||||
|
<section class="module devops-module">
|
||||||
|
<header class="module-header">
|
||||||
|
<div>
|
||||||
|
<span class="module-id">DEV.04</span>
|
||||||
|
<h2>Delivery pipeline</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="story-images">
|
<p>Git-based workflows from repository change to automated deployment.</p>
|
||||||
<figure class="screenshot-exhibit">
|
<div class="module-state"><i></i> LAST RUN PASSED</div>
|
||||||
<figcaption><span>Source control</span><small>Self-hosted repositories in Gitea</small></figcaption>
|
</header>
|
||||||
|
|
||||||
|
<div class="pipeline">
|
||||||
|
<div class="pipeline-flow" aria-label="Deployment pipeline">
|
||||||
|
<div><span>01</span><strong>COMMIT</strong><small>Gitea repository</small></div>
|
||||||
|
<b>→</b>
|
||||||
|
<div><span>02</span><strong>BUILD</strong><small>Woodpecker runner</small></div>
|
||||||
|
<b>→</b>
|
||||||
|
<div><span>03</span><strong>VERIFY</strong><small>Static checks</small></div>
|
||||||
|
<b>→</b>
|
||||||
|
<div><span>04</span><strong>DEPLOY</strong><small>Docker host</small></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pipeline-shots">
|
||||||
|
<figure>
|
||||||
|
<figcaption>GITEA / REPOSITORIES</figcaption>
|
||||||
<a href="assets/images/portfolio-gitea-dashboard.png" target="_blank">
|
<a href="assets/images/portfolio-gitea-dashboard.png" target="_blank">
|
||||||
<img src="assets/images/portfolio-gitea-dashboard.png" alt="Self-hosted Gitea dashboard" />
|
<img src="assets/images/portfolio-gitea-dashboard.png" alt="Gitea repositories dashboard" />
|
||||||
</a>
|
</a>
|
||||||
</figure>
|
</figure>
|
||||||
<figure class="screenshot-exhibit">
|
<figure>
|
||||||
<figcaption><span>Continuous delivery</span><small>Successful Woodpecker pipeline</small></figcaption>
|
<figcaption>WOODPECKER / PIPELINE</figcaption>
|
||||||
<a href="assets/images/portfolio-woodpecker-pipeline.png" target="_blank">
|
<a href="assets/images/portfolio-woodpecker-pipeline.png" target="_blank">
|
||||||
<img src="assets/images/portfolio-woodpecker-pipeline.png" alt="Successful Woodpecker CI pipeline" />
|
<img src="assets/images/portfolio-woodpecker-pipeline.png" alt="Successful Woodpecker CI pipeline" />
|
||||||
</a>
|
</a>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<article class="system-story reverse">
|
<section id="security" class="module security-module">
|
||||||
<div class="story-copy">
|
<header class="module-header">
|
||||||
<p class="story-number">04</p>
|
<div>
|
||||||
<p class="story-label">Platform & recovery</p>
|
<span class="module-id">SEC.05</span>
|
||||||
<h3>The boring work is part of the system.</h3>
|
<h2>Identity & perimeter</h2>
|
||||||
<p>
|
</div>
|
||||||
Dashy keeps service access legible. The wiki records inventory, DNS, restore order,
|
<p>Central authentication, protected services, and automated hostile-traffic decisions.</p>
|
||||||
backup validation, and the steps required to rebuild the environment from a clean host.
|
<div class="module-state"><i></i> ENFORCING</div>
|
||||||
</p>
|
</header>
|
||||||
|
|
||||||
|
<div class="security-layout">
|
||||||
|
<figure class="auth-shot">
|
||||||
|
<figcaption>
|
||||||
|
<span>AUTHENTIK</span>
|
||||||
|
<small>11 protected applications / 2FA enforced</small>
|
||||||
|
</figcaption>
|
||||||
|
<a href="assets/images/portfolio-authentik-applications.png" target="_blank">
|
||||||
|
<img src="assets/images/portfolio-authentik-applications.png" alt="Applications protected by Authentik" />
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div class="security-data">
|
||||||
|
<div class="security-score">
|
||||||
|
<span>ACCESS POSTURE</span>
|
||||||
|
<strong>HARDENED</strong>
|
||||||
|
<small>KEY-ONLY SSH / SSO / TLS</small>
|
||||||
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Dashy</li><li>Wiki.js</li><li>Restic</li><li>Runbooks</li>
|
<li><span>OIDC providers</span><strong>04</strong></li>
|
||||||
|
<li><span>Forward-auth apps</span><strong>07</strong></li>
|
||||||
|
<li><span>2FA policy</span><strong>ENFORCED</strong></li>
|
||||||
|
<li><span>Public SSH password</span><strong>DISABLED</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="story-images">
|
|
||||||
<figure class="screenshot-exhibit">
|
<figure class="crowdsec-shot">
|
||||||
<figcaption><span>Service portal</span><small>Internal tools organized in Dashy</small></figcaption>
|
<figcaption>
|
||||||
<a href="assets/images/portfolio-cloud-dashboard-overview.png" target="_blank">
|
<span>CROWDSEC / ACTIVE DECISIONS</span>
|
||||||
<img src="assets/images/portfolio-cloud-dashboard-overview.png" alt="ByteGeist internal service portal" />
|
<small>Automated hostile source blocking</small>
|
||||||
</a>
|
</figcaption>
|
||||||
</figure>
|
<a href="assets/images/portfolio-crowdsec-decisions.png" target="_blank">
|
||||||
<figure class="screenshot-exhibit">
|
<img src="assets/images/portfolio-crowdsec-decisions.png" alt="CrowdSec active firewall decisions" />
|
||||||
<figcaption><span>Recovery runbook</span><small>Documented rebuild and restore process</small></figcaption>
|
</a>
|
||||||
<a href="assets/images/portfolio-wiki-disaster-recovery.png" target="_blank">
|
</figure>
|
||||||
<img src="assets/images/portfolio-wiki-disaster-recovery.png" alt="ByteGeist disaster recovery runbook" />
|
</div>
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="lessons" class="lessons section-rule">
|
<section class="module recovery-module">
|
||||||
<div class="section-heading compact">
|
<header class="module-header">
|
||||||
<p class="section-index">03 / Lessons</p>
|
|
||||||
<div>
|
<div>
|
||||||
<h2>What actually took work.</h2>
|
<span class="module-id">DR.06</span>
|
||||||
|
<h2>Recovery state</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p>Configuration in Git, persistent data backups, and an explicit rebuild sequence.</p>
|
||||||
|
<div class="module-state"><i></i> RUNBOOK CURRENT</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div class="lesson-list">
|
<div class="recovery-layout">
|
||||||
<article>
|
<div class="recovery-sequence">
|
||||||
<span>01</span>
|
<div><span>01</span><strong>Provision host</strong><small>Ubuntu / firewall / SSH</small></div>
|
||||||
<h3>OIDC is mostly careful bookkeeping.</h3>
|
<div><span>02</span><strong>Restore config</strong><small>Git / Compose / secrets</small></div>
|
||||||
<p>Redirect URIs, claims, group mapping, and user provisioning all need to agree across both ends.</p>
|
<div><span>03</span><strong>Restore data</strong><small>Volumes / databases</small></div>
|
||||||
</article>
|
<div><span>04</span><strong>Validate edge</strong><small>DNS / TLS / identity</small></div>
|
||||||
<article>
|
</div>
|
||||||
<span>02</span>
|
<figure>
|
||||||
<h3>Backups are unfinished until restore is documented.</h3>
|
<a href="assets/images/portfolio-wiki-disaster-recovery.png" target="_blank">
|
||||||
<p>I track data location, dependencies, restore order, DNS, certificates, and validation—not just archive jobs.</p>
|
<img src="assets/images/portfolio-wiki-disaster-recovery.png" alt="ByteGeist disaster recovery runbook" />
|
||||||
</article>
|
</a>
|
||||||
<article>
|
</figure>
|
||||||
<span>03</span>
|
|
||||||
<h3>Observability has to support a decision.</h3>
|
|
||||||
<p>A smaller dashboard that explains an incident is more useful than a wall of metrics nobody checks.</p>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<span>04</span>
|
|
||||||
<h3>Convenience can weaken the boundary.</h3>
|
|
||||||
<p>Every exposed service has a cost. SSO, least privilege, and fewer public endpoints keep that cost explicit.</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="closing">
|
|
||||||
<p class="section-index">Project notes</p>
|
|
||||||
<h2>This environment is still changing.</h2>
|
|
||||||
<p>
|
|
||||||
I use ByteGeist to practice the parts of infrastructure work that tutorials tend to skip:
|
|
||||||
upgrades, failure modes, access policy, noisy logs, and recovery.
|
|
||||||
</p>
|
|
||||||
<a class="button primary" href="https://wiki.casko.dev">Open the documentation <span>↗</span></a>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<a class="brand" href="#top">
|
<div class="footer-mark">
|
||||||
<img src="assets/images/bytegeist-logo.png" alt="" />
|
<img src="assets/images/bytegeist-logo.png" alt="" />
|
||||||
<span>ByteGeist</span>
|
<div><strong>BYTEGEIST</strong><span>KEITH CASKO / INFRASTRUCTURE LAB</span></div>
|
||||||
</a>
|
</div>
|
||||||
<p>An infrastructure lab by Keith Casko, running on Hetzner.</p>
|
<p>PLAIN HTML / CSS / JAVASCRIPT</p>
|
||||||
<p>Built with plain HTML and CSS.</p>
|
<p>STATUS: <span class="status-text">OPERATIONAL</span></p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
<!-- Pipeline Success Test -->
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const timeElement = document.querySelector("#local-time");
|
||||||
|
const refreshElement = document.querySelector("#refresh-indicator");
|
||||||
|
const counters = document.querySelectorAll("[data-count]");
|
||||||
|
|
||||||
|
function updateClock() {
|
||||||
|
const now = new Date();
|
||||||
|
timeElement.textContent = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: "America/New_York",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
}).format(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
let secondsSinceRefresh = 0;
|
||||||
|
|
||||||
|
function updateRefreshState() {
|
||||||
|
refreshElement.textContent = `UPDATED ${String(secondsSinceRefresh).padStart(2, "0")}s AGO`;
|
||||||
|
secondsSinceRefresh = (secondsSinceRefresh + 1) % 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateCounters() {
|
||||||
|
counters.forEach((counter) => {
|
||||||
|
const target = Number(counter.dataset.count);
|
||||||
|
let current = 0;
|
||||||
|
const increment = Math.max(1, Math.ceil(target / 18));
|
||||||
|
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
current = Math.min(target, current + increment);
|
||||||
|
counter.textContent = current;
|
||||||
|
if (current === target) window.clearInterval(timer);
|
||||||
|
}, 45);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClock();
|
||||||
|
updateRefreshState();
|
||||||
|
animateCounters();
|
||||||
|
|
||||||
|
window.setInterval(updateClock, 1000);
|
||||||
|
window.setInterval(updateRefreshState, 1000);
|
||||||
Reference in New Issue
Block a user