RAFAEL REIS case study
llmops · self-hosted

12 years of homelab: my self-hosted AI stack

TL;DR

Stack: Proxmox · ZFS · RTX 3090 (GPU passthrough) · llama.cpp · Qwen · pgvector · Tailscale · Cloudflare Tunnel · Grafana · Borg

The lab that became a method

Calling this a "homelab" is technically accurate the way calling a Boeing a "vehicle" is. The foundation: a Proxmox hypervisor on a dual-Xeon server (48 threads, 160 GB of ECC RAM), ZFS storage, two GPUs on passthrough (one for media, one dedicated to LLM inference) and 40+ services across 3 environments: the local host, an ARM VPS in the cloud, and LXC containers. Everything deployed from git, with the same GitOps pattern that runs my company's SaaS. This is where the pattern was proven first.

A number I find more telling than the 40+ active services: the directory of decommissioned stacks holds twice that. SSO providers, four generations of monitoring stack, orchestrators: tested seriously, compared, and retired with notes. A good homelab isn't the one that hoards services: it's the one that evaluates and kills them.

And it's real, daily use: syncs every 15 minutes, nightly synthesis jobs, an automated morning briefing on weekdays, daily backups. The lab isn't a showcase. It's infrastructure I depend on.

Networking: the answer to "which port did you open?" is none

No port-forwarding on the router. Public exposure goes out through Cloudflare Tunnel, with routing declared by labels on the containers themselves: bringing a service up already publishes it, with an access policy (SSO) in front of the admin panels. Internal traffic rides a Tailscale mesh (zero-trust, per-node identity). At the physical edge, an OPNsense firewall with dual WAN and automatic failover.

The practical effect: from the outside, the attack surface is that of someone with nothing running at home. On the inside, every access has an identity.

the network, from the inside out
internet Cloudflare Tunnel SSO in front of admin panels HOMELAB 40+ services published by labels on the container itself the connection dials out my devices Tailscale mesh · per-node identity 0 open ports on the router
the external surface is that of someone with nothing at home · the arrow points outward on purpose

The AI stack

Inference: 24 GB of VRAM is the currency

A dedicated VM gets the RTX 3090 via GPU passthrough and runs llama.cpp with a model orchestrator in front, a "VRAM scheduler" that loads and unloads models on demand, with eviction cost rules based on measured real-world usage (in my logs, embedding requests outnumber chat by more than 20:1, so the embeddings model is never evicted; it stays hot 24/7).

The daily catalog: an embeddings model and a reranker resident, plus a quantized ~35B-parameter Qwen MoE with 128K context and a quantized KV cache, all of it coexisting in ~20 of the 24 GB. For heavy work, larger models with 256K context (one of them with vision) take the whole card, at 23.7 of 24 GB. This is LLMOps in practice: quantization as a conscious trade-off, a VRAM budget as capacity planning, and orchestration as scheduling: the same infrastructure disciplines, in a new currency.

the VRAM scheduler, on replay
VRAM: 20.4 / 24 GB
qwen 35B · 128K context 27B with vision · 23.7 GB · whole card 0 12 GB 24 GB
boot · bge-m3 (embeddings) resident 24/7 · 1.2 GB
load reranker · 0.9 GB
chat · load quantized qwen 35B 128K · 18 GB
vision job · evict the daily set · load 27B · 23.7 GB
job done · restore the daily set · 20.4 / 24 GB
the scheduler's real sequence · eviction guided by measured usage: embeddings outnumber chat 20:1, so they're never evicted

Optimization with a number attached: moving the embeddings pipeline from CPU to GPU dropped the processing of a large document from ~38 s to ~4.6 s.

RAG and the personal knowledge graph

On top of the inference layer, a personal knowledge graph: 2,500+ pages, thousands of links and timeline entries, with hybrid search, vectors (pgvector) + Portuguese full-text, and 100% of the corpus embedded locally. Every night, a synthesis job walks the graph and writes reflections connecting what came in that day.

That nightly job is the best cost story in this case study: it first ran on cloud APIs, and hit rate limits, costing cents per run, every day. I moved the whole synthesis to the local GPU: from cost per call to the marginal cost of electricity, no rate limits, with data that never leaves the house. Cloud for the frontier, local for recurring volume. Knowing which is which is the job.

Agents: the homelab audits the company

The top floor of the stack is agent runtimes: an agent gateway with tools and memory, and, my favorite, an autonomous pentest agent (open-source, orchestrated by a workflow engine) that runs against the code of my own SaaS, using the local LLM as its brain. The lab at home doing continuous red-teaming of the company, at the cost of electricity. One of its findings became a postmortem and a patch sent upstream.

Run like production (otherwise it's just a pile of containers)

40+active services
3environments
2 yearsof metrics retention
15+postmortems written

Why self-host AI (and what it taught me)

Other case studies

Migrating 50+ live tenants across data centers, with zero downtime Hot replica, atomic cutover, and proof by packet capture, not optimism. A one-person platform team 60+ tenants, two data centers, one engineer. By architecture, not heroics.

Always happy to compare notes with people who run real infrastructure.

LinkedIn