Bigger on the Inside: Building Gallifrey After a Free Server, a WireGuard Grudge, and an AI Co-Pilot
How a gifted six-drive gaming rig, a stubborn WireGuard setup, and an AI companion running inside Orca turned into Gallifrey — a modular home-lab notebook — and the software habits that kept the whole thing honest.
Around the same time all of this was happening, I was also elbow-deep in a completely different experiment: EG Klank, my first real attempt at letting AI agents build a fully functional system from scratch — livestreaming, a sermon archive, multi-tenancy, the works — with room to actually scale. It deserves its own post, and it's getting one. Consider this the mid-credits scene. Patience.
This story starts somewhere less glamorous: a spare bedroom, a stack of drives, and a router that was quietly doing more work than it got credit for.
The gift horse
Someone I know upgraded their gaming rig and, rather than let the old one gather dust, handed it to me. Six 4TB drives, a case, a power supply that had clearly seen a LAN party or twelve. Free hardware has a way of making decisions for you — I hadn't planned to rebuild my home lab that month, but a free server has a gravitational pull all its own.
At the time, "my home lab" was one honest, unglamorous box: an old gaming PC running Ubuntu Server, Portainer, and a handful of stacks. It did its job. Mostly.
The one thing it did not do gracefully was WireGuard. I had it running directly on the Docker host — which sounds sensible until you remember that Docker loves to rewrite iptables rules like it owns the place. Some nights the containers were perfectly reachable over the tunnel and the host itself was a ghost. Other nights it was the reverse. I spent more evenings arguing with routing tables than I'd like to admit, which is a very specific kind of home-lab suffering nobody warns you about.
So before touching the shiny new drives, I fixed the foundation.
Put the edge at the edge
I moved to OpenWrt as the base for everything network-shaped: SSL client VPN, DDNS, and local DNS management. Not because it's the trendiest choice, but because it's the correct one — the edge of your network should live on the thing whose entire job is being the edge of your network, not on a container that also happens to be hosting fourteen other things and can be redeployed by anyone at 2am with a typo.
VPN termination, dynamic DNS, and name resolution now happen on the router. The Docker host went back to doing what Docker hosts are good at: running containers. Nobody has to guess whether a routing headache is a Docker problem, a WireGuard problem, or a "did I edit the wrong compose file" problem anymore, because there's only one place any of those problems can live.
That single decision solved more 2am mysteries than anything else in this whole project.
The upgrade: from one box to an array
With the network sorted, the six gifted drives became an Unraid array: four data drives, one parity drive, and one physical cold spare sitting on a shelf, doing absolutely nothing until the day it's needed. That's the whole point of a cold spare — it's boring on purpose.
A cheapskate's home-lab manifesto, since I clearly have opinions on this:
- Free or gifted compute beats bought compute, every time your patience can absorb mismatched hardware. Nobody asks what generation your CPU is before your lab works.
- Spend actual money where reliability compounds — the network edge, power, and the drives that protect your data — not on whatever has the shiniest benchmark this month.
- A cold spare on a shelf is cheaper than a support contract, and just as effective at 3am.
- The one line item that got dramatically cheaper thanks to AI-assisted development was software. The labour to build a whole ops platform didn't need to be bought. It needed to be directed.
That last point is where this story stops being about hardware.
GitLab, guardrails, and an infrastructure companion
I run my own GitLab for CI/CD and version control — issues, milestones, merge requests, the lot. Nothing exotic, just the same discipline you'd want on any serious codebase, pointed at my own basement instead of someone else's SaaS.
Around this time I started building something I only half-jokingly call my infrastructure companion: an agent living in Orca, running on Claude, wired up with observability across the lab and — deliberately, carefully — enough access to actually drive changes to it. Not a chatbot that suggests commands for me to copy-paste. Something that can see what's happening and act on it.
That's a lot of trust to hand to something that doesn't get tired or ask "are you sure?" unprompted, so the trust is scoped, not blanket:
flowchart LR
subgraph Edge["OpenWrt — the edge"]
VPN[SSL client VPN]
DDNS[DDNS]
DNS[Local DNS]
end
subgraph Compute["Unraid — the array"]
Docker["Docker host<br/>4 data + 1 parity + 1 cold spare"]
end
subgraph Delivery["GitLab"]
VCS["Issues, milestones, MRs"]
CI["CI/CD"]
end
subgraph Companion["Infrastructure companion"]
Orca[Orca]
Claude[Claude]
Orca --> Claude
end
Gallifrey["Gallifrey<br/>single pane of glass"]
Me["Me, mostly making tea"] --> VPN
VPN --> Docker
VCS -->|small, scoped issues| Claude
Claude -->|observes| Gallifrey
Claude -->|drives changes, on a leash| Docker
CI -->|ships| Docker
Gallifrey -->|watches| Docker
Gallifrey -->|watches| Edge
Orca is the part that makes this actually workable rather than a party trick. It's an orchestration layer for agents — it lets me spin up an agent in its own isolated worktree per task, hand it a small, well-defined issue, let it work without babysitting every keystroke, and review the result before anything touches the real lab. Run that a few times in parallel on genuinely independent work, and one operator (me, mostly making tea) suddenly has the throughput of a small team. Run it without the scoping and review step, and you have a small team's worth of ways to break your own network before breakfast.
Which brings me to the actual realization that started this whole project.
The realization that started it all
The companion could see things and do things. What it — and I — could not easily do was know the current, honest state of everything at a glance. Container inventory lived in one UI. The NAS had its own. DNS lived with a provider. The reverse proxy had yet another dashboard. The router had its own panel too, obviously, because why would anything be simple.
Keeping track of the infrastructure itself, and all its moving parts, in one simple pane of glass turned out to be the actual hard problem — harder than teaching an agent to SSH into things. So I built the thing that's been missing this whole story.
That's what brought along Gallifrey. Everything from here is the technical part.
Every home lab starts with a tidy idea. One router. One server. A handful of containers. A mental map that fits on a sticky note.
Then the sticky note grows teeth.
Containers get a management UI. The NAS gets its own UI. DNS lives with a cloud provider. Reverse proxies get another dashboard. Suddenly the most important system in the room isn't any of those tools — it's the story you tell yourself about how they fit together.
Gallifrey is my attempt to write that story in software. It's a modular ops console for a private network: hierarchical IP address management, plus inventory modules that watch the systems you already run. It's named after a place that's bigger on the inside, because that's the design bet — start small, grow by plugging in modules, and never let any one connector become the whole product.
This post is part architecture note, part build diary, and part field guide for anyone who wants to build something similar. I'll keep real hostnames, addresses, and private URLs out of it. The patterns travel. The floor plan of my lab does not need to.
If you remember one sentence: treat inventory as a cache with manners — useful, dated, and never confused with live truth.
The problem in plain language
Imagine your house has five light switches in five rooms, and each switch only tells the truth about its own bulb. You can still live there. You just can't answer a simple question quickly:
What is actually on right now?
That's the home-lab version of operational awareness. The tools are excellent at their local jobs. They're mediocre at giving you one coherent picture.
I wanted a console that could:
- Own address space properly — folders, subnets, IPs — instead of leaving allocations in spreadsheets and muscle memory
- Refresh what hosts, DNS, and proxies look like on a schedule
- Highlight drift: stale containers, dangling DNS names, proxies missing access controls, addresses that exist in inventory but nowhere useful
- Stay inventory-first — observe and reconcile, don't become a second remote control for every upstream API
- Stay honest about time — a sync from five minutes ago is still a snapshot
That last point sounds philosophical. It isn't. It's a product rule. The moment a dashboard pretends a cache is live, operators start making decisions on theater.
A map of the whole idea
Before the stack table, here's the shape of the system. Gallifrey sits in the middle as a read-mostly ops brain. Upstream systems remain the systems of record for their own domains. Gallifrey takes copies, links them to IPAM, and surfaces attention.
flowchart LR
subgraph People
Op[Operator]
end
subgraph Gallifrey
UI[Web UI<br/>Django + HTMX]
Core[Module registry]
IPAM[IPAM]
Mods[Inventory modules]
Dash[Home ops dashboard]
UI --> Core
Core --> IPAM
Core --> Mods
Core --> Dash
end
subgraph Upstream["Systems you already run"]
Docker[Container platform]
NAS[NAS / hypervisor UI]
Router[Edge router]
DNS[External DNS]
Proxy[Reverse proxy]
end
Op --> UI
Mods -->|scheduled sync| Docker
Mods -->|scheduled sync| NAS
Mods -->|scheduled sync| Router
Mods -->|scheduled sync| DNS
Mods -->|scheduled sync| Proxy
Mods -->|link addresses| IPAM
If you're not a software person: think of Gallifrey as a lab notebook that can update itself, not as a remote control that replaces every other remote.
Stack choices, and why they're boring on purpose
Ambitious home-lab software dies in two popular ways:
- It becomes a frontend science fair.
- It becomes a plugin marketplace before it has one good module.
I chose a stack that biases toward shipping modules.
| Piece | What it buys you |
|---|---|
| Django | Auth, ORM, migrations, an admin escape hatch, and a boring place to grow business logic |
| HTMX + server templates | Interactive UI without standing up a separate SPA build |
| Tailwind via standalone CLI | Modern styling without dragging Node into the runtime image |
| Celery + Beat + Redis | Background sync/scan work so page loads stay thin |
| SQLite in development, Postgres in production | Easy local start; durable lab database later |
| A small module registry | Nav, URLs, settings, and home summaries with runtime on/off — without inventing a plugin economy |
| A light PWA shell | Installable UI and offline shell only — never offline data |
The deliberate non-choices matter as much as the choices:
- No SPA framework
- No filesystem plugin discovery
- No "mutate the universe from one button" ambitions in v1
- Network scanning stays conservative: reachability plus reverse DNS, not a free-for-all packet carnival
flowchart TB
Req[Browser]
App[Django app<br/>gunicorn]
Worker[Celery worker]
Beat[Celery beat]
Redis[(Redis broker)]
DB[(Database)]
Req -->|HTTP pages + HTMX partials| App
App --> DB
Beat -->|enqueue due work| Redis
Redis --> Worker
Worker --> DB
Worker -->|talk to upstream APIs / scans| Outside[Lab systems]
Two lanes, one product:
- The request lane answers humans quickly.
- The background lane keeps the notebook up to date.
If you're building your first serious side project, steal that split early. Mixing long-running syncs into web requests teaches your UI to feel tired.
The module registry: bigger on the inside
Gallifrey isn't a plugin host that hunts the disk for surprises. It's a monorepo of Django apps with one extra capability Django doesn't give you for free: runtime enable/disable in the shared shell.
A module registers itself at startup. The database remembers whether it's enabled. Middleware gates the URLs. The nav only shows what's on. Core code stays politely ignorant of module internals.
flowchart TB
subgraph Core
Reg[registry.register]
Gate[Module gate middleware]
Nav[Nav context processor]
Home[Home summary loader]
Settings[Settings page]
end
subgraph Modules
M1[IPAM]
M2[Container hosts]
M3[NAS connector]
M4[Router connector]
M5[DNS inventory]
M6[Proxy inventory]
end
M1 --> Reg
M2 --> Reg
M3 --> Reg
M4 --> Reg
M5 --> Reg
M6 --> Reg
Reg --> Gate
Reg --> Nav
Reg --> Home
Reg --> Settings
Why this shape helps aspiring engineers:
- New work has a recipe instead of a blank canvas
- Parallel contributors (human or agent) can mirror an existing module instead of inventing dialect number seven
- Turning a module off doesn't require a redeploy archaeology dig
Growth looks like this:
flowchart LR
A[Thin API / SSH client] --> B[Reconcile into snapshots]
B --> C[Celery beat schedule]
C --> D[HTMX list + detail UI]
D --> E[Optional home attention]
That pipeline is the whole product strategy in five boxes.
What the modules do
Today the surface looks like this:
- IPAM — folders → subnets → addresses, with utilization views and scheduled reachability checks that refuse to overwrite carefully hand-entered rows
- Container hosts — read inventory from a container management API and link discovered addresses into IPAM
- NAS / host inventory — sync machines, shares, or containers from the box that holds your disks
- Router inventory — bring edge-network reality into the same story
- DNS inventory — snapshot zones and records, then flag suspicious leftovers
- Reverse-proxy inventory — snapshot proxy hosts, certificates, and access-list posture
The home screen isn't a launcher grid of vanity tiles. It's an ops summary: attention items, sync health, IPAM posture, and module shortcuts. Drift is a first-class product surface.
flowchart TB
subgraph Home["Home ops dashboard"]
Att[Attention]
Sync[Sync health]
Addr[IPAM posture]
Mods[Module shortcuts]
end
IPAM[IPAM summaries] --> Att
IPAM --> Addr
Hosts[Host modules] --> Att
Hosts --> Sync
DNS[DNS module] --> Att
Proxy[Proxy module] --> Att
Reg[Enabled modules] --> Mods
Data flow: how truth gets into the notebook
Here's the sync story without romanticizing it.
sequenceDiagram
participant Beat as Celery Beat
participant Redis as Redis
participant Worker as Celery worker
participant API as Upstream system
participant DB as Snapshot tables
participant UI as Operator UI
Beat->>Redis: enqueue due sync/scan
Redis->>Worker: deliver job
Worker->>API: fetch current inventory
API-->>Worker: records / hosts / DNS / proxies
Worker->>DB: reconcile snapshots
Worker->>DB: write sync run + health
UI->>DB: read snapshots + attention
Note over UI,DB: UI shows "as of last sync"<br/>never "this is live reality"
A few design rules fell out of building this:
- Snapshots are snapshots. Name them that way in code and in the UI.
- Manual truth is sacred in IPAM. A scanner may discover. It does not get to vandalize carefully curated rows.
- Credentials are per integration row, not mysterious global soup, and the UI should never echo secrets back for sport.
- Cadence is a product decision. Host inventory may refresh often. DNS may refresh more slowly. Pretending one interval fits all systems is how you invent alert noise.
Deployment pattern: publish an image, run a small fleet
I won't diagram my private network. You don't need it. You need the pattern.
Gallifrey deploys as five cooperating containers from one application image plus managed data services:
| Role | Why it exists |
|---|---|
| Web app | Serves the UI |
| Worker | Runs sync/scan jobs |
| Beat | Schedules those jobs |
| Database | Durable state |
| Broker | Job transport |
Networking follows a classic dual-plane idea:
flowchart TB
subgraph LAN["Reachable lab network"]
Web[Web app]
Worker[Worker]
Humans[Operators / VPN]
Targets[Scan + API targets]
end
subgraph Internal["Private app network"]
DB[(Database)]
Redis[(Broker)]
Beat[Beat]
end
Humans --> Web
Web --> DB
Worker --> DB
Worker --> Redis
Beat --> Redis
Web --> Redis
Worker --> Targets
Beat -.->|no need for LAN| Internal
Why the worker often needs real network reachability: background jobs that ping subnets or call internal APIs can't live forever inside a sealed bridge and still tell the truth about a LAN.
Delivery is intentionally dumb in a healthy way:
flowchart LR
Dev[Commit to main] --> CI[CI builds image]
CI --> Reg[Container registry]
Reg --> Host[Lab host pulls image]
Host --> Run[Recreate app/worker/beat]
CI publishes. The host pulls. There's no need for the application repo to SSH into production like a nervous stage magician. Pull-based updates aren't perfect — pinning and change windows matter as you mature — but for a single-operator lab they keep credentials and deploy mechanics where they belong.
Security posture for a first version can be refreshingly honest:
- Keep the console on the private network
- Prefer VPN over "just expose it" — see: the entire OpenWrt detour above
- Decide early whether TLS terminates in front of the app or whether you're accepting LAN-only HTTP while you learn
- Inventory external DNS and reverse proxies without requiring your ops console to be publicly published through them
Gallifrey watches the edge. It doesn't need to be the edge.
Delegating responsibly: what the companion actually gets to do
This part is for the engineers already using coding agents, and for everyone who keeps hearing that "AI builds infrastructure now" and wonders what that actually looks like on a Tuesday evening.
The Orca-and-Claude companion helped a lot on Gallifrey. It did not replace architecture, and it never got to touch the lab without a leash.
The guardrails, roughly, in the order they matter:
- Small, scoped issues in GitLab. Not "build the DNS module." More like "add the DNS zone snapshot model and the sync task, nothing else."
- Isolated worktrees per task. Every agent works on its own copy, so parallel work doesn't collide until it's ready to.
- An architecture contract, not vibes. The module recipe — how a module registers, how it stores snapshots, what "healthy" means — is written down, and agents are told to follow it, not reinvent it.
- A human review gate before anything merges, let alone deploys.
- Milestones that mark real phases — IPAM MVP, first inventory module, the home dashboard — so "done" means something more specific than "the code compiles."
flowchart TB
Contract[Architecture contract<br/>module recipe + invariants] --> Agent[Coding agent]
Issue[Small GitLab issue<br/>with clear acceptance] --> Agent
Agent --> Draft[Code + tests + docs draft]
Draft --> Human[Human review]
Human -->|approve / reshape| Merge[Merge]
Human -->|gap found| Contract
What scaled beautifully with this setup:
- Issue-sized work, run in parallel, in isolated worktrees
- Pattern mirroring: "make this module look like that module"
- Mechanical expansion: forms, URLs, partials, tests, docs
- Multiple merge requests in flight at once, when the contracts didn't collide
What still needed an actual human:
- Product boundaries: inventory versus mutation
- Auth weirdness in third-party systems
- Sync semantics and freshness rules
- Cross-module health definitions
- Saying no to clever half-features
- Keeping the architecture document truthful
The useful mental model isn't "AI wrote my app." It's:
AI is a force multiplier inside a strong contract. Orca is what lets you run several of those multiplied efforts at once without losing the plot.
Without the contract, you get faster generation of inconsistent surface area. With the contract — and an orchestration layer that keeps each task scoped, reviewable, and honestly reported — you get a factory for modules that still feels like one product, built mostly by one operator who also has a day job.
If you're early in your career, this is the encouraging part: the highest-leverage skill is still taste and structure. Agents amplify whatever system you point them at. Point them at chaos, get faster chaos. Point them at a contract, get a factory.
Lessons worth stealing
Whether you build an ops console, a livestreaming platform, or a personal site that somehow grew opinions, these ideas travel:
- Separate the human path from the background path. Pages shouldn't wait for the whole lab to confess.
- Name your caches. Snapshots, sync runs, "as of" timestamps — language prevents self-deception.
- Prefer a module recipe over a plugin fantasy. Explicit registration beats magical discovery when you're solo or small.
- Keep architecture docs as contracts. Changelogs are for git. Decisions belong in a living document.
- Deploy with boring pipes. Build an image. Publish it. Pull it. Recreate the processes that should change.
- Fix the foundation before the fun part. OpenWrt before Unraid. Network before dashboards. It is never the exciting order, and it is always the right one.
- Use AI for expansion, not for identity. The product still needs your judgment about what not to build — and an orchestration layer only multiplies whatever judgment you already brought.
- A cold spare on a shelf beats a support contract. Budget home labs win on foundations, not on flexing hardware.
Closing
Gallifrey started as a complaint about sticky notes — and a gift horse's worth of hard drives — and ended as a small platform with opinions.
It's still private. It's still growing. That's fine. Not every useful system needs a public demo button on day one. Some systems earn their keep by making the private world clearer.
If you take anything into your own lab this week, take this:
Fix the foundation first. Build the notebook. Keep the notebook honest. Let the modules make it bigger on the inside.
And when an agent offers to generate the next connector in twenty minutes — smile, hand it a small enough issue, and make sure your architecture document is ready to receive it.
Next up: EG Klank — what happens when you let AI agents build an entire livestreaming platform from a blank repository. Bring popcorn.