DEV Community

Nejdet Kadir Bektaş
Nejdet Kadir Bektaş

Posted on

How I Built My Own Home-Lab: A Single-Node Kubernetes Platform

I turned a second-hand mini-PC into a proper, internet-facing application platform: one Kubernetes node running my own apps, deployed automatically on every git push, reachable both on my LAN and (selectively) from the internet — without opening a single port on my router.

This post is the high-level tour. It's not a copy-paste tutorial; it's the map I wish I'd had, with the decisions that mattered, the traps I hit, and links to the official docs for the details.

My hardware: an HP EliteDesk 800 G3 Mini (i5-6500T, 8 GB RAM, 128 GB SSD). Used, cheap, silent, sips power. You do not need a rack.


The one idea that makes it all work: Git is the source of truth

Before any tool, decide this: the cluster should be describable entirely by Git repositories. Every app, every config, every secret (encrypted) lives in version control. The cluster is disposable — if it burns down, you re-apply Git and you're back.

This principle (called GitOps) is what turns a pile of kubectl apply commands into something you can reason about, review, and rebuild.

You: git push ──> CI: test + build image ──> Container Registry
                        │
                        └──> Git: write new image tag ──> ArgoCD watches Git ──> Kubernetes applies it ──> App is live
Enter fullscreen mode Exit fullscreen mode

The golden rule: CI never talks to the cluster. It builds an image and writes one line to Git. A separate agent inside the cluster (ArgoCD) notices the change and applies it. Nothing outside holds cluster credentials.


The stack, and why

Layer Choice Why
OS Debian 12 minimal rock-solid, tiny, no surprises
Kubernetes k3s full k8s in ~1 GB RAM; batteries included (ingress, load-balancer, storage)
GitOps Argo CD the cluster pulls from Git; you never push to it
Ingress Traefik (bundled with k3s) one front door for every app
Certificates cert-manager + a private CA HTTPS on the LAN with your own trusted root
DNS + ad-blocking AdGuard Home serves *.home.lab to the whole house, blocks ads as a bonus
Database CloudNativePG Postgres that heals itself
Public edge Cloudflare Tunnel expose apps with zero open ports
Remote access Tailscale reach the whole lab from anywhere, securely
Secrets SOPS + age encrypted secrets safe to commit to Git

Tip: k3s ships Traefik, a load-balancer (ServiceLB), local storage, and metrics-server out of the box. Don't install a heavier distro and then bolt those on — you'll fight the defaults.


The build, stage by stage (high level)

The whole thing is one ordered runbook. Each stage only depends on the ones above it, so a full rebuild is "run this list top to bottom."

0. Accounts, domains, keys
1. Install Debian, harden SSH
2. Install k3s
3. cert-manager + private CA
4. AdGuard: LAN DNS
5. Container registry
6. Postgres + Redis
7. Argo CD
8. Deploy your first app
9. CI: auto-deploy on push
10. Cloudflare Tunnel: go public
11. Tailscale: remote access
Enter fullscreen mode Exit fullscreen mode

0–2 — Foundation

Reserve a fixed IP for the box in your router's DHCP settings (bind it to the machine's MAC address — don't set a static IP on the host). Install Debian minimal: no desktop, SSH server yes. Then harden SSH to key-only. Install k3s with a one-liner from get.k3s.io. Copy the kubeconfig to your laptop and you're driving the cluster from your desk.

3–5 — The platform

cert-manager mints a private Certificate Authority so every *.home.lab site gets real HTTPS. AdGuard becomes your LAN's DNS server (point your router's DNS at it), so argocd.home.lab resolves everywhere in the house. A small container registry holds your app images.

6–8 — Data and GitOps

Postgres and Redis come up as shared backing services. Then Argo CD — the heart of the whole thing. You hand-apply exactly one Argo CD "app-of-apps," and from then on, new applications appear simply by committing a small file to a Git repo.

9 — The magic moment: automatic deploys

Wire up CI so that every push to main runs your tests, builds an image, pushes it, and writes the new tag back to Git. Argo CD sees the commit and rolls out the new version. You never touch the server.

10–11 — Public and remote

Cloudflare Tunnel publishes chosen apps to the internet on your own domain — outbound-only, so no router ports open and your home IP stays hidden. Tailscale lets you reach the entire lab (SSH, dashboards, everything) from anywhere as if you were home.


The happy path (what a normal day looks like)

Once it's built, shipping a change is astonishingly boring — which is the point:

  1. I edit code, git push.
  2. CI tests, builds, and commits a one-line tag change.
  3. Argo CD notices and deploys it.
  4. A couple of minutes later, the new version is serving traffic — on my LAN and on the public URL.

No SSH. No kubectl. No manual steps. That loop is the entire reason to build this.


Tips I earned the hard way

  • Budget your RAM before you install anything. On 8 GB, every component needs a line in a budget table. My platform idles around 3.5 GB, leaving ~4 GB for apps. The biggest single consumer (Prometheus + Grafana monitoring, ~1.3 GB) I made optional — installed only when I actually need it. Don't run monitoring you're not looking at.
  • The node must not depend on the cluster to boot. Point the host's DNS at a public resolver (1.1.1.1), never at the AdGuard pod — otherwise a reboot deadlocks (the node needs DNS to start the pod that provides DNS).
  • Minimal Debian has no curl. Tiny thing, cost me a confused minute. apt install curl before the k3s script.
  • containerd doesn't read the OS trust store. For a private-registry CA, you must put the cert in k3s's registries.yaml and restart k3s — trusting it at the OS level isn't enough.
  • macOS treats DNS1/DNS2 as a pool, not a fallback. Set a secondary public DNS and some *.home.lab lookups randomly fail (and get cached as "doesn't exist"). Keeping Tailscale connected fixes it permanently via split-DNS.
  • Encrypt secrets, commit them anyway. With SOPS + age, encrypted secrets live safely in Git. Keep the private key in your password manager. This is what makes "rebuild from Git" actually true.
  • Set your BIOS to power on after outage. A shelf server nobody power-cycles must come back by itself.
  • Buy used RAM. The single best upgrade. DDR4 SODIMMs are cheap and dissolve almost every "deferred until later" decision.

The security posture (don't skip this)

The infrastructure perimeter can be genuinely strong: no open ports, one fail-closed public hostname behind Cloudflare's WAF, admin dashboards restricted to LAN/Tailscale only, all secrets encrypted.

But the weakest point is usually the app you put behind it, not the perimeter. My hard-won lesson: the moment you expose an app publicly, audit it — default seeded admin accounts, hardcoded credentials, migrations that run in production. A locked front door doesn't help if the app ships with a spare key taped under the mat.

Rule of thumb: exposing an app to the internet is a one-line change — and so is un-exposing it. Treat "public" as a deliberate, reviewable decision, per app.


Was it worth it?

Completely. For the price of a used mini-PC and a weekend, I have a platform that deploys my side-projects automatically, serves them with real HTTPS on my own domain, and I can rebuild from Git if the SSD dies. More importantly, everything I practice here — GitOps, Kubernetes, secrets management, zero-trust networking — is the same shape as what runs in production at work, just sized for a shelf.

Start smaller than this if you want. Even "k3s + one app + Tailscale" is a fantastic first weekend. Then let it grow — the discipline of budgeting and Git-as-truth scales with you.


Further reading

Happy self-hosting. Your home-lab is the best classroom you'll ever own.

Top comments (0)