Skip to content

Commit d1c6ebf

Browse files
committed
feat: homelab infra post
1 parent 19a03a4 commit d1c6ebf

19 files changed

Lines changed: 467 additions & 37 deletions

File tree

content/about.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

content/homelab.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: "Homelab"
3+
date: 2026-05-19T15:12:07+10:00
4+
draft: false
5+
---
6+
This page outlines the system I run at home for my homelab. I don't quite feel comfortable making the repository public as, although I don't have any secrets committed, there are occasional snippets of personally identifiable information such as location variables. Security through obscurity 👍. The format of this page is inspired by [Shane Dowling's Homelab page](https://shanedowling.com/homelab/), which I will likewise keep updated to reflect any continual changes I make. The repository exists as a private GitHub repository, but also as a Forgejo mirror on my own infrastructure.
7+
8+
---
9+
10+
# Hardware
11+
12+
My current setup exists in a rack-mounted configuration (20 RU) within my living room. From top to bottom of the rack I have:
13+
14+
{{% details "**Router** — Intel N100 mini PC" %}}
15+
Ordered from Aliexpress, my mini PC runs OPNsense with 8 GB RAM and 256 GB storage. This machine is completely overkill for routing but I hope to set it aside for a future Proxmox node to learn kubeadm, run a home testing environment, and host learning environments for the rest of my university subcommittee.
16+
{{% /details %}}
17+
18+
{{% details "**Access Point** — Unifi 6" %}}
19+
PoE-powered from the switch below. Managed via Unifi Controller running on the cluster. It *barely* provides a Wi-Fi connection to the rest of my house with the occasional dropouts in my room.
20+
{{% /details %}}
21+
22+
{{% details "**Switch** — 8-port GbE unmanaged" %}}
23+
Provides PoE to the AP and Ethernet to the rest of the rack.
24+
{{% /details %}}
25+
26+
{{% details "**Compute Node** — ATX Box" %}}
27+
The main k3s node, built from my old server's hardware.
28+
29+
- **CPU** - 12th Gen Intel Core i7-12700K (12 cores). Integrated graphics handle Jellyfin hardware transcoding.
30+
- **Memory** - 64 GiB DDR4. Overkill for most of the workloads I run, but very helpful for my modded Minecraft servers.
31+
- **Storage** - 14 TB across salvaged drives from the old NAS, configured as a BTRFS RAID1 pool for mirrored redundancy.
32+
{{% /details %}}
33+
34+
{{% details "**UPS**" %}}
35+
In the mail.
36+
{{% /details %}}
37+
38+
---
39+
40+
# Storage
41+
Storage sits in its own section intentionally as it's an area I expect to build upon significantly. The current setup is quite simple: around six drives in a BTRFS RAID1 pool on the k3s compute node, mounted at `/mnt/payload`. All applications use `local-path` storage rather than persistent volume claims, which keeps things straightforward for now but limits flexibility.
42+
43+
The longer-term plan involves a proper JBOD or SAN setup with block storage and storage class management. This will allow me to support things like CNPG for database management and pave the way for a multi-node configuration. More on that in [What I Want to Work On Next](#what-i-want-to-work-on-next).
44+
45+
---
46+
47+
# Cluster Architecture
48+
49+
The cluster runs as a single-node Kubernetes setup using k3s, with Ansible playbooks to simplify system updates and document node bootstrapping.
50+
51+
## Secrets
52+
53+
Bitnami Sealed Secrets manages most secrets. Encrypted secrets are stored directly in the repository alongside the rest of the configuration, and are decrypted server-side using a private key that is not committed to Git. Having version-controlled secrets alongside everything else is appealing as recovery from a cluster meltdown only requires restoring that private key.
54+
55+
## Renovate
56+
57+
Renovate runs periodically against the GitHub repository to check for image updates. PRs are reviewed and approved manually before the new tags are applied and manifests are pushed to the cluster. Fully automated updates are tempting, but the risk of breaking changes has kept it manual for now.
58+
59+
## Ingress
60+
The cluster uses Istio as the service mesh and reverse proxy via the Kubernetes Gateway API. This replaced Istio VirtualServices, which were less portable as the new API is now in a stable enough state that fit all my use-cases.
61+
62+
## SSO
63+
Most services support SSO via SAML or OIDC. Authentik handles the specific flows I need well enough, though Ory is on the radar as version-controlled client definitions are a really appealing idea.
64+
65+
## Remote Access
66+
Tailscale works reliably across all my devices. Self-hosting Headscale gives me control over the tailnet's coordination server and integrates with the SSO setup. The OPNsense router runs the Tailscale plugin as an exit node for LAN access, while a separate Tailscale exit node (routed through a Gluetun/Mullvad deployment) provides a more private browsing option.
67+
68+
---
69+
70+
# Services
71+
72+
## Infrastructure
73+
- ForgeJo
74+
- Adguard Home
75+
- Cert-manager
76+
- Multus - Required for Unifi Controller and AdGuard
77+
78+
## Media
79+
- Immich
80+
- Navidrome and Beets
81+
- SearXNG
82+
83+
## Productivity
84+
- Outline
85+
- Memos
86+
- Grist
87+
- Vaultwarden
88+
- Radicale
89+
90+
## Automation and Communication
91+
- n8n
92+
- ntfy
93+
- Tuwunel
94+
- Home assistant
95+
96+
## Monitoring
97+
Monitoring deserves it's own explanation as its a bit of its own ecosystem.
98+
99+
### Metrics
100+
Prometheus scrapes metrics cluster-wide, with Grafana providing dashboards and visualisations. As someone who enjoys data and its presentation, this is one of the more satisfying parts of the setup.
101+
102+
### Logs
103+
Loki handles log aggregation, with Grafana Alloy running as a `DaemonSet`, collecting both metrics and logs from across the cluster.
104+
105+
### Traces
106+
Planned. Tempo will be added here once the rest of the observability stack is more settled to complete the LGTM stack. Distributed tracing should complement the existing metrics and log data nicely.
107+
108+
### External Data Collection
109+
Various exporters exist within the cluster to scrape information such as SearXNG performances or my music listening habits.
110+
111+
Additionally, an Apollo Air-1 monitors room air quality over ESPHome and exports readings to Prometheus. A Home Assistant automation opens the room's ventilation when CO₂ crosses 1000 ppm.
112+
113+
---
114+
115+
# What I Want To Work On Next
116+
- **Storage overhaul**: A JBOD system or SAN with block storage and proper storage class management would unlock things like CNPG for database management and make multi-node expansion viable. Alongside this, an encrypted buddy backup arrangement with a friend would address the lack of any meaningful automated backup. Currently photos go to Google Photos and application storage is occasionally copied to an external drive by hand.
117+
- **Managed switch and VLANs**: Network segmentation for guest or untrusted devices would improve isolation.
118+
- **Observability improvements**: Adding Tempo for distributed tracing (see above) and wiring up Alertmanager to ntfy would give faster visibility into service outages.

hugo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ I am fond of pigs. Dogs look up at us. Cats look down on us. Pigs treat us as eq
3030
startLevel = 2
3131
endLevel = 4
3232
ordered = false
33+
[markup.goldmark]
34+
[markup.goldmark.renderer]
35+
unsafe = true
3336

3437
[caches]
3538
[caches.images]

layouts/_default/single.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{{ define "main" }}
2-
<article>
2+
<article class="single-page">
33
<header class="page-header">
44
<h1>{{ .Title }}</h1>
55
</header>
66
<div class="post-content">
77
{{ .Content }}
88
</div>
9+
<p class="last-updated">Last updated: <time datetime="{{ .Lastmod.Format "2006-01-02" }}">{{ .Lastmod.Format "2006-01-02" }}</time></p>
910
</article>
1011
{{ end }}

layouts/partials/footer.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
<p>
33
&copy; {{ now.Year }} {{ .Site.Params.author | default .Site.Title }}
44
{{ with .Site.Home.OutputFormats.Get "RSS" }}&middot; <a href="{{ .Permalink }}">RSS</a>{{ end }}
5+
&middot; Built with <a href="https://gohugo.io/">Hugo</a>
56
</p>
67
</footer>

layouts/partials/header.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<nav>
33
<a href="{{ "/" | absURL }}" class="site-title">{{ .Site.Title }}</a>
44
<div class="nav-links">
5-
<a href="{{ "/about" | absURL }}">about</a>
5+
<a href="{{ "/homelab" | absURL }}">homelab</a>
66
</div>
77
</nav>
88
</header>

layouts/posts/single.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ <h1>{{ .Title }}</h1>
2323
<div class="post-content">
2424
{{ .Content }}
2525
</div>
26+
<p class="last-updated">Last updated: <time datetime="{{ .Lastmod.Format "2006-01-02" }}">{{ .Lastmod.Format "2006-01-02" }}</time></p>
2627
</article>
2728
{{ end }}

layouts/shortcodes/details.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<details>
2+
<summary>{{ .Get 0 | markdownify }}</summary>
3+
<div class="details-content">
4+
{{- .Inner -}}
5+
</div>
6+
</details>

public/css/main.css

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,16 @@ header nav {
136136
Main
137137
-------------------------------------------------------- */
138138
main {
139-
padding-bottom: 5rem;
139+
padding-bottom: 2rem;
140+
}
141+
142+
.last-updated {
143+
color: var(--text-muted);
144+
font-style: italic;
145+
font-size: 0.8rem;
146+
margin-top: 1.5rem;
147+
margin-bottom: 0;
148+
opacity: 0.7;
140149
}
141150

142151
/* --------------------------------------------------------
@@ -209,12 +218,17 @@ footer a {
209218
Page header (tags index, about, search)
210219
-------------------------------------------------------- */
211220
.page-header {
212-
margin-bottom: 2rem;
221+
margin-bottom: 0;
222+
}
223+
224+
.single-page .post-content > p:first-child {
225+
margin-top: 0;
213226
}
214227

215228
.page-header h1 {
216-
font-size: 1.25rem;
229+
font-size: 1.5rem;
217230
font-weight: bold;
231+
line-height: 1.35;
218232
color: var(--text);
219233
}
220234

@@ -300,6 +314,39 @@ footer a {
300314
margin-top: 0.25rem;
301315
}
302316

317+
details {
318+
margin: 0.4rem 0;
319+
}
320+
321+
summary {
322+
cursor: pointer;
323+
list-style: revert;
324+
}
325+
326+
summary::marker {
327+
color: var(--text-muted);
328+
}
329+
330+
.details-content {
331+
margin-top: 0.5rem;
332+
padding: 0.4rem 0 0.4rem 1rem;
333+
border-left: 2px solid var(--link);
334+
}
335+
336+
.details-content p {
337+
margin: 0.4rem 0;
338+
}
339+
340+
.details-content ul {
341+
margin: 0.4rem 0 0 0;
342+
padding-left: 1.25rem;
343+
list-style: disc;
344+
}
345+
346+
.details-content li {
347+
margin: 0.35rem 0;
348+
}
349+
303350
.post-content p {
304351
margin: 1.4rem 0;
305352
}

0 commit comments

Comments
 (0)