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
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
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:
- I edit code,
git push. - CI tests, builds, and commits a one-line tag change.
- Argo CD notices and deploys it.
- 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 curlbefore 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.yamland 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.lablookups 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
- k3s docs — the distribution that makes single-node k8s pleasant
- Argo CD getting started — the GitOps engine
- Cloudflare Tunnel — public exposure without open ports
- Tailscale — the mesh VPN for remote access
- SOPS — encrypt secrets you can safely commit
- cert-manager — automated certificates, including a private CA
Happy self-hosting. Your home-lab is the best classroom you'll ever own.
Top comments (0)