I Used Claude Code to Build My Private Cloud
Updated April 6, 2026 — originally “I Used Claude Code to Build My Homelab.” Rewritten to cover the full K3s migration that happened the same weekend.
Most AI coding content falls into one of two buckets: vague hype (“AI wrote my entire app!”) or defensive dismissal (“it’s just autocomplete”). Neither is useful. This is a practical account of what it actually looks like to use Claude Code as a serious collaborator on infrastructure work — including the config file I use to make it behave.
The premise: treat it like a contractor
The mental model that made everything click for me: Claude Code isn’t a chatbot you ask questions. It’s a contractor you brief. The difference matters.
A contractor doesn’t need you to explain the same preferences on every job. You give them a spec once — I want exposed brick, no drop ceilings, permits pulled before demo — and they work within that. You check in at decision points. You don’t micromanage the drywall.
Claude Code has a mechanism for exactly this: CLAUDE.md. A markdown file at the root of your project (or globally in ~/.claude/CLAUDE.md) that gets loaded into every session automatically. It’s your briefing document. The AI reads it before touching anything.
Here’s mine, lightly redacted:
# Vex — Evatt Labs AI Assistant
You are **Vex**, Jordan's personal hacker-chick AI assistant. Think Felicity Smoak
meets the hacker from every crime procedural — technically sharp, a little chaotic,
talks fast, gets shit done.
---
# Jordan's Global Claude Code Preferences
## Context
- Solo founder, Evatt Labs
- Go backend, Next.js/TypeScript frontend, K3s private cloud, GitHub Actions CI/CD
- Linux-primary, self-hosted datacenter, no public cloud assumptions
## Code Preferences
- Go: idiomatic, explicit error handling always, no panic in library code
- TypeScript: strict types, no `any`, functional components only
- Prefer explicit over clever. Readability over brevity.
- Error handling: always explicit, always logged with context. No silent failures.
## Infrastructure Context
- 3-node K3s cluster, Flux GitOps, Helm charts
- Secrets via Vault + External Secrets Operator
- Site deploys to Cloudflare Pages; everything else is LAN/Tailscale only
## Homelab
- Helicon: primary workstation + GPU compute (RTX 5090)
- Kalgan: rack server, K3s control plane + platform services
- Trantor: storage + database node, ZFS tank ~22TB RAIDZ2
- NEVER restart a host without explicit confirmation. All disks are LUKS-encrypted —
a reboot requires physical console access at the rack.
If a reboot seems necessary, stop and ask. There is almost certainly a better way.
- Prefer live remediation: reload services, recycle containers, remount filesystems.
That last section — the LUKS reboot warning — has already saved me twice. Without it, the natural AI instinct when something is broken is “have you tried turning it off and back on again.” With it, I get “here are three ways to fix this without a reboot” instead. That’s worth the five minutes it took to write.
What I actually built
The homelab runs a 3-node K3s cluster:
- Kalgan (rack server): K3s control plane, Caddy reverse proxy, Harbor container registry (standalone), platform services
- Trantor (storage node): PostgreSQL (CloudNativePG), MinIO, Dragonfly, NATS — all ZFS-backed
- Helicon (workstation): RTX 5090 GPU compute, where I work, the Claude Code sessions
On top of that: Keycloak for SSO, Vault for secrets, Forgejo for git, Grafana/Prometheus/Loki for observability, Vaultwarden for passwords. The public site deploys to Cloudflare Pages — the homelab itself has zero public ingress.
All infrastructure is managed as code in a dedicated homelab repo with Flux GitOps.
A real example: the entire K3s migration
Here’s what a real marathon session looks like.
The original setup was Docker Compose on each host — rsync the repo, SSH in, docker compose up -d. It worked, but it was fragile and didn’t scale. In one session that stretched from Saturday night into Monday morning, Vex and I migrated the entire stack to K3s:
- Bootstrapped a 3-node K3s cluster with k3sup
- Deployed MetalLB, Longhorn, and cert-manager as the foundation
- Cut over the live site to K3s with zero downtime (Cloudflare Tunnel as the switching point)
- Migrated the full monitoring stack
- Stood up PostgreSQL, MinIO, Dragonfly, NATS on Trantor
- Deployed Keycloak, Vault, External Secrets Operator
- Got NVIDIA GPU detection working in K3s for the RTX 5090
- Finally moved the site to Cloudflare Pages, making the homelab fully private
Along the way, we hit real problems — the kind that eat hours:
- Caddy image pull chicken-and-egg: Harbor’s HTTPS goes through Caddy, but Caddy’s image lives in Harbor. Solved by importing the image directly via
k3s ctr images import. - Let’s Encrypt rate limits: Caddy on an emptyDir lost certs on every restart, burning through the 5 certs/168h limit. Fixed with Longhorn PVCs for persistence.
- Keycloak OOMKilled: needed 4Gi memory (not the 1Gi I initially set). Keycloak is heavy on cold boot.
- NVIDIA GPU detection: five rounds of troubleshooting — containerd config, NVML library paths, device node mounts — before
nvidia-smiran inside a K8s pod.
None of these were things the AI could have predicted from reading docs. All of them were things it could diagnose and fix once I showed it the error.
What doesn’t work
In the interest of not being hype content:
It can’t see what it can’t read. If something is misconfigured in a way that only manifests at runtime — a Caddy config that parses but doesn’t route correctly, a pod that starts but behaves wrong — Claude Code can’t observe it. You still need to know how to read logs and trace problems yourself.
Context limits are real. Long sessions accumulate cruft. I compact aggressively mid-task rather than starting fresh, but there’s still a point where the AI loses track of earlier decisions. For complex multi-day work, I write a brief plan at the start and keep it updated.
It optimizes for what you measure. If your CLAUDE.md doesn’t say “don’t add dependencies without flagging them,” it will add dependencies. If it doesn’t say “don’t refactor surrounding code when fixing a bug,” it will refactor. The briefing matters.
The actual value
The ROI isn’t “AI writes code instead of me.” It’s compression of the boring parts.
I know what I want. I know how the system should work. What I don’t want to do is spend 45 minutes looking up the exact Helm values for Keycloak’s PostgreSQL backend, or remember the right containerd config path for K3s on Arch, or trace through why a DaemonSet can’t find libnvidia-ml.so. Claude Code handles that layer. I handle the decisions.
For solo infrastructure work especially — where the blast radius of a mistake is real and the feedback loop is slow — having an AI that respects your constraints and asks before doing anything irreversible is genuinely useful.
The LUKS warning in my CLAUDE.md isn’t paranoia. It’s experience.
The full CLAUDE.md and project config live in the evattlabs repo.