← Holocron Logs

Why I Didn't Migrate Ghost to Azure: The Blog Platform Decision

The original plan was Ghost Pro to Azure VM. A real AZ-104 deployment, production operations, unified monitoring. The actual decision was Ghost Pro to Astro Content Collections on Netlify. Here is why the plan changed, what was built instead, and what that decision says about how infrastructure choices should actually be made.

Why this matters beyond the homelab: Every infrastructure decision involves tradeoffs between cost, complexity, capability, and fit. The ability to evaluate a technically sound plan, recognize when it is solving the wrong problem, and choose a simpler path that achieves the actual goal is a skill that shows up in every engineering role. This post is about that decision.


The Original Plan

The Holocron Logs blog ran on Ghost Pro at $9/month. It worked. Posts published, the custom domain resolved, the Ghost Content API fed blog post cards into the portfolio. The only problem was the cost and the dependency on Ghost’s infrastructure.

The original migration plan, written in April 2026, was to move to a self-hosted Ghost instance on an Azure VM. The reasoning was solid:

The plan had a timeline, a resource group, VM sizing (B2s, 2 vCPU, 4GB RAM), an NSG configuration, a Telegraf integration via Tailscale, and an n8n alert pipeline routing Azure Monitor webhooks to Discord. It was thorough. It was technically sound.

It was also solving the wrong problem.


What the Azure Plan Actually Was

Stepping back from the technical detail: the Azure plan was paying $15-20/month to run a Node.js application on a cloud VM so I could call it a portfolio project.

Ghost on Azure would have required:

And at the end of all of that, the output would be: a blog. The same blog, just running on infrastructure I now have to maintain.

The portfolio value argument also fell apart under scrutiny. By the time the migration was planned, the fleet already had:

The actual AZ-104 work was already happening in more meaningful ways. A Ghost VM would be redundant proof of something already demonstrated better elsewhere.


The Decision Point

The question that changed the direction: what problem am I actually trying to solve?

The answer was not “I need a Ghost VM on Azure.” The answer was:

  1. Stop paying Ghost Pro $9/month
  2. Have a blog that lives under tima.dev/blog as part of the portfolio
  3. Write and publish technical posts without friction
  4. Not maintain a separate CMS application

Reframed that way, the answer was obvious. The portfolio already ran on Astro, deployed on Netlify, auto-deploying from GitHub. Adding a blog to an existing Astro project is an afternoon of work. The result is a static site with no server to maintain, no database, no Node.js process to keep alive, zero hosting cost, and a publish workflow that is just: write markdown, push to GitHub, done.


What Was Actually Built

The migration was not just swapping platforms. It became a complete blog infrastructure build:

Astro Content Collections: Markdown files in src/content/blog/ with a TypeScript schema validating frontmatter. Every post has a title, date, summary, type (writeup/project/field-notes/career), act, tags, featured flag, and draft flag. Zod validates the schema at build time. A malformed post fails the build before it reaches production.

Ghost export conversion pipeline: A Python script converted the Ghost JSON export (41 published and draft posts) to properly frontmatted markdown. The script handled HTML to markdown conversion, slug renaming (post01 through post023 became real titles), factual corrections (n8n workflow count updated from 58 to 84+, career length corrected to 8 years), Ghost URL replacement, and capitalization normalization across all 41 posts. Five iterations to get it clean.

Blog index with Act 1 series pinning: The blog landing page renders a pinned series section for the nine Act 1 foundation posts in fixed reading order, a featured carousel for highlighted Act 2 posts, and a date-sorted filterable grid for everything else. The filter bar sorts by content type: writeups, projects, field notes, career.

Individual post renderer: The [slug].astro template handles markdown rendering with full prose styling: headings, code blocks with yellow left border, tables, blockquotes, inline images for diagrams. The same CSS variables as the rest of the site so everything looks native.

Phase flag system: src/config/phases.ts has a blogLive: boolean flag. While the blog was being built, blogLive: false showed a countdown timer on the home page and nav. Flipping it to true activates the nav link and removes the countdown. One line change to go live.


The Numbers

Ghost ProAzure VMAstro/Netlify
Monthly cost$9$15-20$0
MaintenanceLow (managed)High (self-managed)None (static)
Publish workflowGhost admin UIGhost admin UIGit push
Content portabilityJSON exportJSON exportNative markdown
CMS dependencyYesYesNo
Build failures from bad contentNoNoYes (Zod validates)
Hosting dependencyGhost serversAzure VM uptimeNetlify CDN

The Zod validation row is worth pausing on. Ghost and a self-hosted Ghost VM both let you publish malformed content silently. The Astro build fails if a post has a missing required field, a type value outside the enum, or a date that cannot be parsed. That is a better content pipeline for a technical blog.


What the Azure Plan Teaches

The original plan was not wrong. It was a reasonable first response to the problem statement “migrate off Ghost Pro.” It identified real AZ-104 terrain, real portfolio value, real technical work.

The mistake was not questioning whether the plan was the minimum viable path to the actual goal. A Ghost VM on Azure is a more complex, more expensive, harder to maintain solution than the problem required. The fact that it would exercise Azure skills is not a good enough reason to choose it when those skills are already being demonstrated through the backup pipeline, the Entra Connect deployment, and the existing Azure tenant.

The infrastructure decision that serves the goal best is usually the one that is easiest to operate long-term, not the one that looks most impressive in a plan document.

The blog is now at tima.dev/blog. It costs nothing to run. Every post is a markdown file in a git repository. Ghost Pro is cancelled.


← Back to Holocron Logs