Part 1 of the series: Building a Self-Hosted AI Development Platform
5 min read

Building a Self-Hosted AI Development Platform — Part 1: Why Build One?

Part 1 of the Forge series: turning an old dual Xeon workstation into a documented, rebuildable home platform for AI-assisted development, self-hosted Git, and automation

Part 1: Why Build a Self-Hosted AI Development Platform?

This is the first post in a series about a machine I’ve started calling Forge: an old ASUS dual Xeon workstation that now runs my Git hosting, CI, automation, monitoring, media, and storage — and acts as the home base for my AI-assisted development workflow.

The series will get into the weeds of Proxmox, ZFS, Docker, Cloudflare Tunnel, and Forgejo. But this post is about the part that usually gets skipped: what I was actually trying to achieve, what worked straight away, what didn’t, and — more importantly — why I chose the routes I did when the original plan fell apart. Because it did fall apart, and the platform is better for it.

The Goal

The pitch to myself was simple. I had a dual Xeon workstation board with 32GB of RAM, two 256GB Samsung SSDs, and two 6TB hard drives gathering dust. Meanwhile, my development life was scattered: client repos on GitHub, experiments on my laptop, backups wherever, and a growing pile of AI tooling that had no consistent place to live.

The goal was one platform that could handle:

  • AI experiments and automation harnesses
  • Self-hosted Git with CI for client and personal projects
  • WordPress and Astro development tooling
  • Media, photo, and document storage
  • Monitoring, backups, and dashboards

And one hard rule from the start: the whole thing had to be documented and rebuildable. Every service defined in a Git-tracked Compose file, every architecture decision written down with its reasoning, and a build log kept as I went. If the machine died tomorrow, the repository should be enough to bring it all back.

What We Initially Achieved

The early build went surprisingly smoothly. Proxmox VE went onto the first SSD, the second SSD became fast storage for containers, and the two 6TB drives were paired into a ZFS mirror — forge-hdd — for backups, media, photos, and anything else bulky. Getting storage right before installing services turned out to be one of the best early calls; retrofitting a storage layout under a dozen running containers is nobody’s idea of fun.

On top of that: a Docker LXC container with Portainer for stack management, Homepage as a dashboard, and Uptime Kuma for monitoring. Then the services started stacking up — Open WebUI for an AI chat interface, Forgejo backed by PostgreSQL and Redis for Git hosting, Grafana and Prometheus for metrics, MinIO for S3-compatible object storage, Navidrome for music streaming, and n8n for workflow automation.

One structural decision paid for itself almost immediately: every service joined a shared Docker network, with Caddy routing requests by container name. Adding a new service to the platform became a two-line Caddyfile change — music.example.com reverse-proxies to navidrome, done. No port juggling, no IP addresses to remember. That single pattern is the difference between a platform and a pile of containers.

Problem One: The AI Plan Hit a Wall

Here’s the uncomfortable bit. This was pitched as an AI development platform, and the original plan leaned on local models — Ollama running open-weight LLMs, with Open WebUI as the front end.

The dual Xeons said no. Not literally — everything installed and ran fine — but CPU-only inference on decade-old server silicon is painfully slow for real development work. Token generation that takes tens of seconds per response is a curiosity, not a workflow. I considered a GPU upgrade, priced it up, and asked the harder question: what is this machine actually good at?

The answer reframed the whole project. Old enterprise hardware is genuinely bad at fast LLM inference, but it’s genuinely good at orchestration, storage, CI, and running two dozen services without breaking a sweat. So I made the call: stop hosting models locally, and use hosted models — Claude and GPT — via API instead. Ollama was removed and the model volumes deleted. The server’s job isn’t to be the AI; it’s to be everything around the AI — the Git hosting, the automation pipelines, the knowledge storage, the harnesses that hosted models plug into.

I could have sunk £500+ into a GPU to run models that would still lag behind frontier hosted ones. Instead, that reasoning is written down in the repo’s DECISIONS.md, and the door stays open: if dedicated GPU capacity ever arrives, local inference can come back. That’s the real value of documenting decisions rather than just configurations — six months from now I won’t have to reconstruct why the “AI platform” doesn’t run local models.

Problem Two: Publishing Services Without Exposing My House

A self-hosted platform you can only reach from your sofa is half a platform. I wanted Forgejo, the dashboard, and the AI interface reachable from anywhere. The traditional answer — port forwarding on the home router with dynamic DNS — means publishing your home IP address to the world and trusting every exposed service’s login page. No thanks.

The route I chose was Cloudflare Tunnel plus Caddy. A single cloudflared container makes an outbound connection to Cloudflare; a wildcard hostname sends all of *.example.com down the tunnel to Caddy, which routes to the right container by name. No open router ports, no exposed home IP, and HTTPS handled at Cloudflare’s edge. Adding a public service is just adding a Caddy site block.

Two hard-won rules came out of this:

Keep the admin plane private. Proxmox and Portainer — the interfaces that can destroy everything — never got public hostnames. They’re reachable only over Tailscale. Everything else user-facing sits behind Cloudflare Access, so even “public” services need identity checks before a request reaches my network. A wildcard tunnel makes it easy to publish everything; the discipline is choosing not to.

Tunnel DNS records are not normal DNS records. When SSH pushes to Forgejo timed out, my instinct was to flip the DNS records to “DNS only” to bypass the proxy. That took every published service offline — those records are CNAMEs into Cloudflare’s tunnel infrastructure, with no public origin IP behind them to fall back to. The correct fix was a dedicated tunnel route for SSH with cloudflared on the client side. Lesson logged, and now written into the decisions file as a “never do this” note.

Problem Three: The Platform Outran the Repository

The subtlest problem had nothing to do with hardware or networking. It was drift.

When you’re on a roll, clicking “deploy” in Portainer is faster than writing a Compose file, committing it, and deploying from the repo. Do that a few times and the live platform quietly moves ahead of its documentation. The machine works — but you can no longer explain it, and you certainly can’t rebuild it.

The fix was partly process and partly honesty. The repository has a GAPS.md that explicitly lists what’s live but not yet captured in Git — the tunnel token, a hand-edited DNS config, a Grafana password that differs from its Compose placeholder. An end-of-session rhythm updates the build log, changelog, decisions, and gaps files. It sounds bureaucratic for a homelab, but it’s the entire reason this blog series can exist: the story was written down as it happened, mistakes included.

Why Self-Host Git at All?

Fair question — GitHub exists and is free. But Forgejo turned out to be the keystone service. Client repositories now live canonically on Forge, with CI running on my own runner. GitHub isn’t gone; each repo gets a Forgejo-managed push mirror to GitHub, which does two jobs — off-site backup and deploy trigger for Cloudflare Pages — without ever being a second source of truth. One workflow file runs identically on Forgejo Actions and GitHub Actions. Full ownership, with the cloud’s convenience kept exactly where it earns its place.

Where the Series Goes Next

That’s the shape of the thing: old hardware doing what old hardware is good at, hosted AI doing what it’s good at, and a Git repository holding it all together. Upcoming posts will cover the build step by step — installing Proxmox on the Xeon board, the ZFS storage layout, the Docker and Portainer foundation, self-hosted Git with Forgejo, and safely publishing it all through Cloudflare Tunnel and Caddy.

If you’ve got an old server board making you feel guilty from a shelf, this series is for you.

Next: Part 2 — Installing Proxmox on Old Dual Xeon Hardware