- khodax.com runs on my K3s cluster: Ghost + MySQL, GitOps via ArgoCD, a custom theme version-controlled in my self-hosted GitLab
- Claude Code handles half the editorial work through a custom MCP server wired into Ghost's Admin API
- Renovate broke the site twice in one week — here's exactly what happened and how I hardened the pipeline
This blog runs on the same infrastructure as my Jellyfin, my Pi-hole, and my n8n instance. No Ghost(Pro), no Vercel, no $9/month managed plan. Just one more pod in a K3s cluster that's already running thirty-six others.
Some of you will think that's overkill — a simple blog doesn't need that much complexity. You're not wrong. I did it anyway, and here's how.
Why not just Ghost(Pro)?
Because I already have the cluster. Seven K3s nodes running 24/7 for my personal services, hosting costs already sunk, and a GitOps stack (ArgoCD plus a version-controlled config repo) managing everything else. Adding Ghost was one more manifest line, not a new account to manage.
The more honest reason: I wanted a real testbed. A project with actual stakes — DNS, TLS, bilingual routing, SEO — to try out an idea I'd been sitting on for a while: having Claude Code write and publish the blog with near-full autonomy. No managed host lets you do that.
The architecture
Nothing exotic, but everything's wired together: Ghost runs as a pod in the blog namespace, MySQL sits in the same namespace, Traefik is the ingress in front, Cloudflare handles DNS/proxy in front of Traefik. All of it declared in YAML in a Git repo, synced automatically by ArgoCD — I never run kubectl apply by hand on this app.
The FR/EN routing lives in routes.yaml, a Ghost config file that defines URL collections:
routes:
/: /
collections:
/:
permalink: /{slug}/
filter: tag:-en
/en/:
permalink: /en/{slug}/
filter: tag:en
taxonomies:
tag: /tag/{slug}/
author: /author/{slug}/Tagging a post en is enough to move it under /en/{slug}/ and drop it out of the French feeds — no plugin, no theme-side logic. That's the kind of detail that sold me on Ghost over a more locked-down CMS: config lives in a plain versionable file, not a proprietary database.
The custom theme, under Git
The theme (khodax-theme) is hand-written Handlebars, no Ghost base theme underneath. It sat in a local folder with zero version control until early July — a shortcut that cost me, more on that below.
It's a proper Git repo now, pushed to my self-hosted GitLab, with a deploy script:
./scripts/deploy.sh
# → builds a clean zip in dist/
# → uploads it via the Ghost Admin API (JWT)
# → activates the theme
# → reminds me to verify the render with a cache-busted requestEvery notable change is a commit plus a tag. If a deploy breaks the render, I can finally run git diff against the last working version — before, I had to guess.
Claude Code as writer, through a homemade MCP server
The most unusual part of this setup: I built a small MCP server (ghost-mcp, plain Node.js) that exposes Ghost's Admin API to Claude Code — create a draft, set tags, upload a feature image, publish. A scheduled routine (Tuesdays and Thursdays at 8am) reads my editorial roadmap, writes the article in French, adapts it into English, sources a freely-licensed photo, and publishes both versions without me touching anything.
I review after the fact, not before. That's a deliberate choice, not an oversight — I wanted to find out whether an AI could hold an editorial cadence over time without constant supervision. So far, it's holding. This very article, in fact, is written by that routine.
Renovate gave me two outages for free
Honestly, the most instructive part of this whole project is the two times it fell over in production.
The first: Renovate merged a MySQL major bump (8.4 → 9.7) while I wasn't watching closely enough. MySQL 9.7 migrated the data on its first boot, then crashed on an authentication flag that version had removed. My instinct was to roll back to the previous tag — bad move. MySQL flatly refuses to downgrade a database that's already been migrated to a newer major. The only way out was forward, not back. Ghost sat without a database for several hours while I figured that out.
The second was nastier: that same Renovate run bumped Ghost to major version 6. Ghost 6 changed how Handlebars exposes context in templates — {{content}} and {{title}} called outside the {{#post}} block no longer resolve to anything. My theme, written against Ghost 5, called those variables at the root. Result: every page on the site literally rendered the word "undefined" instead of its content, while every HTTP status code stayed a cheerful 200. A health check that's just a curl and a status code sees nothing wrong.
Since then: database major version bumps (MySQL, Postgres, MariaDB) require manual approval through Renovate's dependency dashboard, no more auto-merge. And my post-deploy verification checklist now greps the actual page content, not just the response code.
The backups almost weren't enough
A nightly job dumps Ghost's MySQL database at 3am — plain mysqldump, nothing fancy. During the MySQL outage described above, that job failed (no database, nothing to dump), leaving me with a several-days-old dump as my only safety net.
While digging in to assess the potential damage, I noticed something that gave me a small chill: this blog's very first article had been published at 4pm, and the last successful dump dated from 3am that same day. Had I needed to restore, that article would simply have vanished. Luckily the MySQL roll-forward worked and the live database survived — I never actually had to test that scenario.
That's still a gap I haven't properly closed: a daily dump protects against total loss, not against thirteen hours of content between two backups. It's on the list, no date attached yet.
What I'd do differently
Put the theme under Git on day one instead of three months in. And set up Renovate's database guardrails before the first major bump, not after the first incident.
If you're thinking about self-hosting your own blog on a cluster you already own: version your theme from the start, lock down database majors in your update tool, and test your post-deploy verification by deliberately breaking something — before it breaks itself on you some random Tuesday night.