Part 1: Why Build a Self-Hosted AI Development Platform?
This is the first post in a series about a machine I call Forge. Forge is an old ASUS dual Xeon workstation. It now runs my Git hosting, CI, automation, monitoring, media, and storage. It is also the base for my AI-assisted development workflow.
The series goes deep into Proxmox, ZFS, Docker, Cloudflare Tunnel, and Forgejo. This post covers the part that most build logs skip. What was the goal? What worked immediately? What failed? And why did I select these routes when the first plan collapsed? The plan did collapse, 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. All of it collected dust. At the same time, my development life was scattered. Client repositories lived on GitHub. Experiments lived on my laptop. Backups lived wherever they landed. A stack of AI tools had no consistent home.
The goal was one platform for all of it:
- AI experiments and automation harnesses
- Self-hosted Git with CI for client and personal projects
- WordPress and Astro development tools
- Media, photo, and document storage
- Monitoring, backups, and dashboards
One hard rule applied from the start. The platform must be documented and rebuildable. Every service gets a Git-tracked Compose file. Every architecture decision gets a written reason. A build log grows with the work. If the machine dies tomorrow, the repository must be enough to make it again.
What We First Achieved
The early build went well. Proxmox VE went onto the first SSD. The second SSD became fast storage for containers. The two 6TB drives became a ZFS mirror named forge-hdd for backups, media, photos, and other large data. The storage went in before the services, and that was one of the best early decisions. A storage redesign under a dozen live containers is painful work.
On top of that came a Docker LXC container with Portainer for stack management, Homepage as a dashboard, and Uptime Kuma for monitoring. Then the services stacked up. Open WebUI gave an AI chat interface. Forgejo, with PostgreSQL and Redis, gave Git hosting. Grafana and Prometheus gave metrics. MinIO gave S3-compatible object storage. Navidrome streamed music. n8n ran workflow automation.
One structural decision paid off almost immediately. Every service joined a shared Docker network, and Caddy routed requests by container name. A new service became a two-line Caddyfile change. For example, music.example.com proxies to navidrome, and that is the whole change. No port lists, and no IP addresses to remember. That single pattern separates a platform from a pile of containers.
Problem One: The AI Plan Hit a Wall
Now the uncomfortable part. I pitched this as an AI development platform, and the first plan depended on local models. Ollama ran open-weight LLMs, and Open WebUI sat in front as the interface.
The dual Xeons said no. Everything installed and ran. But CPU-only inference on old server silicon is very slow for real development work. A response that takes tens of seconds is unusable for iterative work. I priced a GPU upgrade. Then I asked the harder question: what is this machine actually good at?
The answer changed the whole project. Old enterprise hardware is bad at fast LLM inference. It is good at orchestration, storage, CI, and two dozen services at the same time. So I made the call: stop local model hosting, and use hosted models — Claude and GPT — through APIs instead. Ollama went away, and the model volumes went with it. The server’s job is not to be the AI. Its job is everything around the AI: the Git hosting, the automation pipelines, the knowledge storage, and the harnesses that hosted models plug into.
A GPU worth £500 or more can run models that stay permanently behind the frontier hosted ones. The reasons are written in the repository’s DECISIONS.md, and the door stays open. If dedicated GPU capacity arrives later, local inference can return. Six months from now, I do not have to reconstruct why the “AI platform” runs no local models.
Problem Two: Publish Services Without an Exposed House
A self-hosted platform that only works from your sofa is half a platform. I wanted Forgejo, the dashboard, and the AI interface reachable from anywhere. The traditional answer is port forwarding on the home router with dynamic DNS. That publishes your home IP address to the world, and it trusts every exposed login page. No thanks.
The selected route 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, and Caddy routes to the correct container by name. No open router ports, no exposed home IP, and Cloudflare’s edge does the HTTPS. A new public service is one more Caddy site block.
Two hard rules came out of this work.
Keep the admin plane private. Proxmox and Portainer can destroy everything, so they never got public hostnames. They answer only over Tailscale. Every other user-facing service sits behind Cloudflare Access, so even “public” services demand identity before a request reaches my network. A wildcard tunnel makes it easy to publish everything, so each public hostname stays a deliberate decision.
Tunnel DNS records are not normal DNS records. When SSH pushes to Forgejo timed out, my instinct was to set the records to “DNS only” and bypass the proxy. That took every published service offline. Those records are CNAMEs into Cloudflare’s tunnel infrastructure, and no public origin IP exists behind them. The correct fix was a dedicated tunnel route for SSH with cloudflared on the client. The lesson now lives in the decisions file as a “never do this” note.
Problem Three: The Platform Outran the Repository
The most subtle problem had nothing to do with hardware or networks. It was drift.
When the work moves fast, the “deploy” button in Portainer is faster than a commit. Do that a few times, and the live platform moves ahead of its documentation. The machine works, but you can no longer explain it, and you cannot rebuild it.
The fix was part process and part honesty. The repository has a GAPS.md file that lists what is live but not yet captured in Git. Examples include the tunnel token, a hand-edited DNS configuration, and a Grafana password that differs from its tracked placeholder. An end-of-session routine updates the build log, the changelog, the decisions, and the gaps file. It sounds bureaucratic for a homelab. It is also the entire reason this series exists. The story went into files as it happened, mistakes included.
Why Self-Host Git at All?
Fair question — GitHub exists and is free. But Forgejo became the keystone service. Client repositories now live canonically on Forge, and CI runs on my own runner. GitHub did not go away. Each repository gets a Forgejo-managed push mirror to GitHub, which does two jobs: off-site backup, and deploy trigger for Cloudflare Pages. It is never a second source of truth. One workflow file runs identically on Forgejo Actions and GitHub Actions. Full ownership, with the cloud kept exactly where it earns its place.
Where the Series Goes Next
Old hardware does what old hardware is good at, hosted AI does what it is good at, and a Git repository holds it all together. The next posts cover the build step by step: Proxmox on the Xeon board, the ZFS storage layout, the Docker and Portainer foundation, self-hosted Git with Forgejo, and safe publishing with Cloudflare Tunnel and Caddy.
If an old server board makes you feel guilty from a shelf, this series is for you.