Foyn
Articles

The homelab

TL;DR: Two Raspberry Pi 5s at home. One serves this site, a friend group's Slack bot and a few private apps. The other runs Claude Code. Everything I ask for, typed or spoken on my phone, goes to an orchestrator agent that passes it to the right coding session. The session's work comes back as a pull request, which merges and deploys itself once the checks pass.

What it does

The homelab is one Git repository and two small computers. The repository holds the apps, my notes and the instructions the agents work from. These are the parts people use:

I rarely open a laptop to change any of this. I ask for a change from my phone, and the rest of this article is how that request becomes a deployed change.

Two machines

Both are Raspberry Pi 5s with NVMe drives. plex (4 GB) is production: it serves the sites and apps. raspi (16 GB) runs Claude Code: the orchestrator, the coding sessions, and the two apps that call Claude themselves, voice and meetboard.

plexjonasfoyn · phemten · hannahelle · utilsprod
raspiClaude Code · the orchestrator · voice · meetboard
Production follows a label. Deploy workflows run on whichever Pi's GitHub runner carries the prod label, and each stack's tunnel connector is a container in that stack. Moving production to the other Pi means copying the data, moving the label and redeploying, with no DNS or router change.

A visit

Every hostname points at Cloudflare, which terminates TLS and puts Cloudflare Access in front of anything private. Nothing at home listens for the internet: each stack runs its own cloudflared, which dials out to Cloudflare and holds the connection open, and visitors' requests ride back down it.

You, in a browser HTTPS Cloudflare edge TLS · DNS · Access in front of private apps home router · no open ports plex · production raspi · agents jonasfoyn this site + health dashboard phemten friends' site + Slack bot hannahelle Hanna's site utils feature flags for the others voice spoken prompts to the orchestrator meetboard a live meeting whiteboard
Tunnel
The arrow shows who opened it: the stack's cloudflared, from inside the house.
Request
Rides back down the open tunnel to the one app that serves its hostname.
Router
Only ever sees outbound connections, so it needs no port forwards.
One tunnel per stack. Each stack opens its own connection to Cloudflare, so one can be redeployed, or moved to the other Pi, without touching the others.

A top-level folder in the repo is a stack: one Docker Compose project, one tunnel, one deploy workflow and one set of secrets, all named after the folder. Stacks copy code instead of sharing it, so a folder can be handed to other people as it is. The phemten folder already is, to its players. voice and meetboard are the exceptions: they run the Pi's own Claude Code, so each is a plain system service on raspi, and only its tunnel is a container.

A request

I don't start coding sessions myself. Everything I ask for goes to one orchestrator agent on raspi, either typed in the Claude app or spoken while I hold the iPhone's Action Button. A spoken prompt is transcribed on the Pi with Whisper first, about 5 seconds for 11 seconds of speech.

The orchestrator keeps track of every Claude Code session that is running and what each one was asked to do, and decides where each new prompt belongs. A follow-up goes to the session that already has the context. Anything new gets a fresh session, in the homelab repository or in one of my work repositories, and the orchestrator sends me its link. If I name the target ("continue the recipe one", "new session"), it does that; otherwise a small, fast model compares the prompt with what each session is working on.

Most of the value is here. I can send several unrelated requests during a walk, and each one lands with the session that should handle it. I don't keep track of what is open; the orchestrator does. When it is unsure, it starts a new session: a session too many costs a branch, and a prompt in the wrong session muddles that session's work.

Claude app typed Action Button spoken Orchestrator agent tracks every session · routes each prompt Continue a running one New in homelab New in a work repo Pull request the change + updated notes triggers CI gate type-check · build · smoke · lint · tests green Squash-merge to main code and notes land together dispatches Deploy what changed runner labelled prod · compose --wait healthy Live the next session reads main
Orchestrator
Receives every prompt and picks the session for it.
Gate
The only thing that can block a merge: each site the PR touches must type-check and build, containers must pass a smoke test, the Python must lint and pass its tests, and the phemten folder must stay free of host details, because its players can read it.
Memory
Notes and lessons ride in the same PR as the code, so the next session starts from what this one learned.
Sessions run side by side. Each session gets its own branch and checkout, so they don't collide, and a merge redeploys only the stacks whose files changed.

More on the routing rules: Voice. More on the sessions' setup: Agent Harness & Vault.

Memory lives in the repo

Claude Code keeps nothing between sessions, so what it needs to know is stored in the repo, and every change to it goes through a pull request like any other change (the dashed loop above):

Why

Sites on hardware I own, and a place to find out how much everyday work agents can take over. What works here I take to my day job: the orchestrator already starts sessions in my work repositories.