N

This site is the case study.

No Vercel, no Netlify. The page you are reading was built by CI, pushed to a registry, and reconciled by ArgoCD onto a self-hosted Talos Kubernetes cluster in Oslo. The status below is read from that cluster.

checking the cluster…

Serving build

dbcaf27

pinned to commit SHA

ArgoCD app

Synced

healthy · auto-sync

Cluster nodes

6 / 6 ready

Talos Linux · Kubernetes

TLS certificate

auto

auto-renews · Let's Encrypt

How this page reached you.

Every request crosses this chain. Each hop maps to a manifest in the repo.

You

HTTPS request to nordbye.it

TLS 1.3

DNS

Cloudflare DNS points straight at the cluster's public IP

no CDN in front

Cilium LB

L2-announced VIP, eBPF load-balancing into the cluster

LoadBalancer IPAM

Traefik

Gateway API HTTPRoute, certificate from cert-manager

gateway.networking.k8s.io

portfolio pod

Hardened Node runtime serving the Next.js app

read-only rootfs

How a commit becomes this page.

Nothing is applied by hand. A push to main is the only deploy action that exists. ArgoCD reconciles the rest.

01

git push

Commit to main in the homelab monorepo

02

GitHub Actions

Builds the image and updates the manifest tag in the same run

03

GHCR

Image pushed and pinned to the commit SHA, never :latest

04

ArgoCD

Detects the manifest change in Git and syncs the Application

app-of-apps

05

Talos cluster

Rolling update, zero downtime. You're looking at the result.

genesis

A live pulse, no cluster keys in the open.

This site runs as a Next.js server, but the pod serving it still has no access to the Kubernetes API. A small CronJob inside the cluster gathers the facts every few minutes, from the Kubernetes API, ArgoCD, and cert-manager, and writes them to a ConfigMap. The API reads that ConfigMap and serves it at /api/v1/infra.

The page fetches that endpoint client-side. If the fetch fails, the tiles fall back to a build-time snapshot and say so. The page never breaks because the homelab is having a bad day.

design decisions

Serving a status endpoint doesn't mean handing it cluster credentials. The web pod only reads a ConfigMap the publisher writes; it holds no Kubernetes API access of its own. The publisher's RBAC reads the objects it reports on, pinned to resource names where the API allows it, and writes one ConfigMap.

The pill checks the timestamp too. Data older than 15 minutes is reported as stale rather than shown as operational.

GET /api/v1/infrarefreshed every 5 min
{
  "generatedAt": "2026-07-13T05:55:03Z",
  "build": "72088b9",
  "deployedAt": "2026-07-12T19:33:47Z",
  "argocd": {
    "sync": "Synced",
    "health": "Healthy",
    "syncedAt": "2026-07-12T19:32:48Z"
  },
  "nodes": { "ready": 6, "total": 6 },
  "versions": { "talos": "v1.11.6", "kubernetes": "v1.34.0" },
  "cert": { "notAfter": "2026-09-25T11:41:28Z" },
  "history": [{ "d": "2026-07-13", "ok": 71, "total": 71 }, ...]
}

What keeps it honest.

The moving parts behind the diagrams above. Every manifest lives in the homelab repo.

Talos Linux

Immutable, API-managed Kubernetes OS. No SSH, no shell, no drift.

Cilium

eBPF CNI, L2 announcements and LB-IPAM for bare-metal VIPs.

Traefik

Gateway API implementation. HTTPRoutes, not Ingress annotations.

ArgoCD

App-of-apps GitOps. Git is the only write path to the cluster.

cert-manager

ACME certificates issued and rotated without human hands.

External Secrets

Secrets synced from an upstream store. None live in Git.

Prometheus + Grafana

kube-prometheus-stack, with dashboards versioned as code.

Loki + OTel

Log aggregation and traces via the OpenTelemetry collector.