Building My Home Infrastructure with Claude Code — An Honest Account
In Article 1 I wrote that building my infrastructure taught me how to navigate the pitfalls of Claude Code safely. That wasn't an empty promise. This article is where I deliver on it.
What I've built: a fully self-hosted blog stack — Ghost as a headless CMS, Astro as a static frontend generator, Gitea Actions as the CI/CD pipeline, Nginx as a reverse proxy, and Cloudflare as the only external dependency. Everything runs on two Raspberry Pis in the home network, secured with a firewall architecture that holds its own against most corporate environments.
Claude Code was part of the build. But not as an autopilot — as an assistant I watched over at every single step.
The Setup: Small, But Serious
Before we get to Claude Code, a quick look at the infrastructure itself. Because that's the context that shapes everything else.
graph TD
Internet([🌐 Internet]) -->|HTTPS| CF[Cloudflare\nZero Trust Tunnel]
CF -->|Encrypted| UCG[UniFi UCG Max\nFirewall / Router]
UCG -->|DMZ VLAN| Pi4
subgraph Heimnetz["🏠 Home Network"]
subgraph DMZ["DMZ Zone"]
Pi4[Raspberry Pi 4\nGhost CMS\nNginx\nOllama]
end
subgraph LAN["LAN Zone"]
Pi5[Raspberry Pi 5\nGitea\nNAS / Samba]
end
Pi4 <-->|Internal| Pi5
end
UCG -->|LAN VLAN| Pi5
Dev([💻 MacBook\nDevelopment]) -->|SSH / local| Pi5
Dev -->|SSH / local| Pi4
What's running here:
- Pi 5 (16GB): Gitea as a Git server and CI/CD runner, NAS via Samba, fully in the LAN — no direct internet access
- Pi 4 (4GB): Ghost CMS headless, Nginx as reverse proxy, Ollama for local LLM inference — in the DMZ, but only reachable via Cloudflare. All services run in Docker containers.
- UniFi UCG Max: Firewall, zone architecture (LAN / DMZ / IoT), firewall rules, traffic monitoring
- Cloudflare Zero Trust Tunnel: The only external dependency — deliberately
Why Cloudflare Is the Only External Dependency
No open port in the router. No DynDNS. No self-managed TLS certificate that expires and causes a 3am outage. The Cloudflare Tunnel establishes an outbound connection from my Pi to Cloudflare's infrastructure — no port is exposed externally, there's no direct attack surface.
That's a deliberate security decision, not a convenience. Running a home server with an open port 443 exposes you to a very different risk profile than someone who applies Zero Trust as an architectural principle.
Security That Holds Its Own Against Corporate Environments
I occasionally hear the objection: "A home lab isn't a serious setup." I disagree.
What's implemented here:
- Zone architecture (DMZ, LAN, IoT) with explicit firewall rules between zones — no implicit trust
- Encryption at the communication layer — TLS for all external connections, SSH for internal
- Encryption at the hardware layer — encrypted volumes on the Pis
- Zero Trust network access via Cloudflare — no perimeter thinking
- Infrastructure as Code — all configurations versioned in Gitea, reproducible
- Automated deployments with manual approval gates — no push-to-production without review
- Full traffic monitoring via UniFi — every connection is visible
With two users, there's really only one word to say about performance: enough.
The Stack: How the Pieces Fit Together
flowchart TD
classDef cloud fill:#1e3a5f,color:#93c5fd,stroke:#93c5fd,stroke-width:1.5px
A[📝 Write article\nin Ghost] -->|Webhook| B[Gitea\nRepository]
B -->|Trigger| C[Gitea Actions\nCI/CD Pipeline]
C -->|Build| D[Astro\nStatic Site\nGenerator]
D -->|Deploy| E[Dev instance\nfor review]
E -->|QG1: Code review| F[👤 Approval 1]
F -->|QG2: Visual review| G[👤 Approval 2]
G -->|Deploy| H[Nginx Prod\nDocker]
H -->|Cache invalidation| I[Cloudflare\nTunnel]
I -->|HTTPS| J[🌐 Reader]
class F cloud
class G cloud
Ghost runs as a headless CMS in a Docker container — no theme, just the Content API. Articles are written and managed in Ghost, but the frontend is completely decoupled from it.
Astro with the AstroPaper theme generates static HTML pages from the Ghost content. Static means: no server-side rendering, no PHP, no database queries on page load. Fast, secure, low maintenance.
Gitea Actions handles the CI/CD pipeline: Ghost fires a webhook to trigger a build, Gitea Actions pulls the content via the Ghost Content API, Astro builds the static site. Two quality gates stand between writing and publishing — nothing goes live without explicit approval.
Claude Code in Action: What I Actually Did
Now for the actual subject. How did I use Claude Code during the build — and what did I learn from it?
The Ground Rule: Only What I Can Evaluate
I set myself a clear rule: Claude Code only gets involved where I can judge the result myself. That sounds obvious — but in practice it isn't always. The temptation is real to just take a generated YAML block because it "looks plausible."
I deliberately didn't do that. Every step was approved only after I understood what it does.
What Claude Code Did Well
Gitea Actions workflows were a clear strength. YAML syntax is verbose and error-prone to write — a single wrong space is enough to break everything. In operation it's very stable and reliable. Claude Code delivered working scaffolding that I then adapted and reviewed.
# Example: Generated workflow scaffold for Astro build
name: Build and Deploy
on:
repository_dispatch:
types: [ghost-webhook]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build Astro site
run: npm run build
env:
GHOST_API_URL: ${{ secrets.GHOST_API_URL }}
GHOST_CONTENT_API_KEY: ${{ secrets.GHOST_CONTENT_API_KEY }}
Nginx configurations for reverse proxy and SSL termination — Claude Code provided solid starting points. The Cloudflare-specific headers I had to add myself — exactly the scenario I described in Article 1: Claude Code doesn't know my context unless I provide it.
Bash scripts for deployment, backup, and monitoring — this is where Claude Code was most reliable. Contained tasks, well-defined inputs and outputs, easy to review.
Where I Had to Correct Things
Security-relevant configurations — this is where I was most critical. Claude Code generated Nginx configurations that were technically correct but not security-hardened. Missing security headers, overly permissive CORS settings, default values that have no place in production.
That's not a criticism of Claude Code. It's confirmation of the core argument from Article 1: the model doesn't know my security requirements unless I explicitly put them in the prompt.
Gitea-specific quirks — Gitea Actions is very similar to GitHub Actions, but not identical. Claude Code generated GitHub Actions syntax multiple times that simply doesn't work in Gitea. After the first time, I always mentioned it explicitly in the prompt: "This runs on Gitea Actions, not GitHub Actions."
Infrastructure as Code structure — Claude Code produced working but unmaintainable scripts. Too many hardcoded values, no parameterisation, no error handling. I always cleaned that up — and learned a lot in the process.
What I Took Away From It
This is the actually valuable part. Not the generated code — but what I learned through critically reviewing it.
When I review an Nginx block that Claude Code generated, I have to understand why every directive is there. That forces me to go deeper into the documentation than I might have without the AI starting point. Claude Code didn't take the work away from me — it gave me something to think about.
That's the usage pattern I'd recommend: Claude Code as a conversation partner and starting point, not as an autopilot.
The Result: A Pipeline That Works
After several weeks of building, review cycles, and adjustments, the stack is running stably. Here's the full deployment flow with both quality gates:
sequenceDiagram
participant T as Thomas
participant G as Ghost CMS
participant GA as Gitea Actions
participant AS as Astro Build
participant DEV as Dev instance
participant AP1 as Quality Gate 1\nCode Review
participant AP2 as Quality Gate 2\nVisual Review
participant PROD as Nginx Prod
participant CF as Cloudflare
T->>G: Publish article
G->>GA: Fire webhook
GA->>AS: Trigger Astro build
AS->>AP1: Build complete → Code review
T->>AP1: ✅ Approve code
AP1->>DEV: Deploy to dev instance
T->>DEV: Visual review
T->>AP2: ✅ Approve visually
AP2->>PROD: Deploy to production
PROD->>CF: Cache invalidation via API
Note over CF: Readers see new version
Two quality gates — code review and visual review on the dev instance — before anything goes to production. Every arrow in this diagram is a step I understand, have reviewed, and can stand behind. That was the goal.
Conclusion: Build It Yourself, Understand It Yourself, Own It Yourself
What I took away from this project goes beyond the tech stack.
First: a home lab can — with the right architectural approach — be a serious infrastructure. Zone architecture, Zero Trust, Infrastructure as Code, automated pipelines with manual gates — none of that is an enterprise exclusive.
Second: Claude Code is an excellent tool for exactly this kind of context — when used correctly. As a starting point, as a conversation partner, as an accelerator for tasks you can evaluate yourself. Not as a substitute for your own understanding.
Third: the review process isn't the overhead — it's the value. I learned more about Nginx, Gitea Actions, and Bash by critically working through Claude Code output than I would have from passively reading documentation.
Self-hosting isn't nostalgia or protest for me. It's a deliberate choice for control, understanding, and sovereignty — over my data, my infrastructure, and my tech stack.
This article reflects my personal views exclusively and has no connection to any professional affiliation.