Farewell Ghost. Hello Astro on Cloudflare!
I first set up Ghost on a DigitalOcean droplet back in 2013. Over the years I upgraded it, nursed it through Ghost 0.5->6.0 migrations, kept up with Ubuntu patches/upgrades, and generally did the kind of server babysitting that quietly eats up an evening here and there. Ghost is a fine piece of software. I admire the company’s philosophy and they also have a hosted version. DigitalOcean droplets have worked flawlessly for me as well. That said, running a self-hosted Node app on a VPS for a small blog? It’s a lot of moving parts for not a lot of upside. I finally took some time to look around at the range of options, including Ghost’s own SaaS offering.
As a result, I finally pulled the trigger and moved everything to a static site built with Astro, deployed to Cloudflare Pages.
Why leave self-hosted Ghost
A few things converged:
- Maintenance fatigue. Whenever I would check server health or upgrade Ghost, something needed attention — an Ubuntu security patch, an expired cert, a MySQL hiccup after a reboot, running out of disk space on the droplet, or a Ghost upgrade that required a specific Node version. None of these were hard to fix, but they added up to a tax I paid just to keep the lights on.
- Secure by default. A static site on a global CDN has less of an attack surface for me to worry about than a VPS I am kind of maintaining.
- Performance by default. A static site on a global CDN is always going to be faster than a Node app on a single VPS in one region.
- Writing workflow. Ghost’s editor is nice, but I found myself wanting to write in my own editor and version control my posts. Markdown in a git repo beats a browser-based CMS for the way I actually work, and makes it easier for AI agents to help.
- AI tooling makes migration and design improvements cheaper. A migration like this would have taken more time than I wanted to spend a couple of years ago. I had a bunch of theme customizations and deployment tooling custom built for my Ghost-based setup. With AI-assisted coding, both the data migration (converting Ghost’s Mobiledoc export to clean Markdown) and iterating on a fresh UI went dramatically faster — especially for a low-stakes personal blog like this. The cost of switching has never been lower.
- Ghost’s direction. Ghost has been adding features like memberships, newsletters, and paid subscriptions — all great if you’re building a publication or trying to scale your personal brand into a business, but not things I needed for a fun little blog. The gap between what Ghost was becoming and what I actually used it for kept widening, which also made Ghost’s managed cloud offering hard to justify.
- Cost. A $6/month droplet doesn’t sound like much, but for a site that could just as easily be flat files served from a CDN for free, it felt wasteful.
None of these were deal-breakers on their own. Together, they made the case pretty clear.
Why Astro + Cloudflare (for this phase)
I evaluated options against a few practical criteria: keep the site static, keep writing in Markdown + git, preserve URLs, add content related validation guardrails, framework maturity, framework flexibility for future improvements, and remove ongoing ops work.
- Static-first delivery. I’m using Astro as an SSG and deploying to Cloudflare Pages. Content is rendered at build time and served as static files from Cloudflare’s CDN.
- Developer-native writing flow. Posts are Markdown files in-repo, so writing, reviewing, and publishing are just editor + git.
- Build-time guardrails. Astro content collections with Zod validate frontmatter, and I added a custom post quality gate to enforce minimum word counts, description length, and featured images.
- Clean migration fit. Slugs are preserved, dynamic post/tag routes are generated at build time, RSS/sitemap are generated in Astro, and Ghost-era URL patterns are handled with Cloudflare redirects.
- Low-maintenance operations. No Ubuntu/Node/MySQL/Ghost patching/upgrading.
For now this is intentionally a static-first architecture: I’m not using Astro’s more advanced server-driven features yet (no SSR adapter, no per-request rendering, no runtime database-backed content, no auth/membership backend). If requirements change later, I can add those incrementally.
The migration itself
The actual content migration was more tedious than difficult:
- Export from Ghost. Ghost has a JSON export that includes all posts, tags, and metadata. I wrote a quick script to convert each post from Ghost’s Mobiledoc format to Markdown, preserving frontmatter fields.
- Image wrangling. Ghost stores images on the server’s filesystem. I downloaded everything from
/content/images/, reorganized into apublic/images/content/YYYY/MM/structure, and updated image paths in the Markdown files. - URL preservation. This was non-negotiable. Every existing post needed to keep its slug so inbound links wouldn’t break. Astro’s file-based routing made this straightforward — the slug in the frontmatter drives the URL.
- Redirects. Ghost has a few URL patterns (like
/tag/pages and the/rss/feed) that needed redirects or reimplementation. I set up Cloudflare redirect rules for the Ghost-specific paths and built tag pages and an RSS feed natively in Astro. - DNS cutover. Once the Cloudflare Pages site was building and looking right, I pointed the domain’s DNS to Cloudflare. The actual cutover was anticlimactic — which is exactly how you want infrastructure changes to go.
What I gained
Coming from self-hosted Ghost on DigitalOcean — and deliberately skipping Ghost Pro — Astro + Cloudflare fit my needs well:
| Ghost + DigitalOcean | Ghost Pro (hosted) | Astro + Cloudflare | |
|---|---|---|---|
| Monthly cost | ~$6 | $9+ | $0 |
| Basic features | Newsletter, social commenting, paid member management, posts, pages, theming | Newsletter, social commenting, paid member management, posts, pages, theming | Posts, pages, theming |
| Deploy theme/structure | git push + SSH restart | Managed | git push |
| Deploy posts | Browser editor | Browser editor | git push |
| Writing | Browser editor | Browser editor | Text Editor + Markdown |
| Version control | Theme only (posts in database) | None (database) | Git (theme + posts) |
| Post/structure/theme validation | Syntax check + custom checks | Unknown | Schema compliance check + AI quality gates (custom) |
| AI workflow integration | Limited (database-backed content) | Limited (database-backed content) | Native (plain files in git) |
| Global latency | Single region | CDN (Fastly) | CDN (Cloudflare) |
| Server maintenance | Ongoing | None | None |
Figure 2: A side-by-side comparison of hosting options
The cost savings are nice but secondary. The real win is the workflow. Writing in my editor, committing, pushing, and having the site build and deploy automatically — that’s a workflow that gets out of my way instead of being one more thing to manage.
What I lost
To be fair, a few things were easier with Ghost:
- The admin UI. Ghost’s editor and admin panel are genuinely well-designed. If you’re a non-technical writer or you want to hand off posting to someone who isn’t comfortable with git, Ghost is hard to beat.
- Dynamic features out of the box. Memberships, newsletters, comments — Ghost has all of this built in. I didn’t use most of it, but if you do, moving to a static site means finding replacements or dropping them.
- Image management. Dragging and dropping images into a post editor is nicer than manually placing files in a directory and typing out Markdown image syntax. This is probably the one thing I miss day to day.
For a personal blog where I’m the only author and I’m comfortable with git, these trade-offs are easy to make. Your math might be different.
A few things I’d do differently
If I were doing this migration again:
- Set up the quality gate earlier. I added the build-time post quality checks after migrating all the content, which meant retroactively fixing (or exempting) a bunch of old posts. Establishing the quality bar first and migrating content to meet it would have been cleaner.
- Test redirects more systematically. I caught most of the Ghost URL patterns, but a few slipped through. Running a link checker against the old sitemap would have been a more reliable way to verify coverage.
After basic migration, the fun began
Once the migration was done and the site was HTML, CSS, and Astro components in a git repo, iterating on the UI with an AI agent became even more fun. I was able to quickly:
- Add dark mode support
- Iterate on responsive layout variations
- Put parallax scrolling stars in the header
- Build custom components like favorite quotes and recent GitHub forks/stars sidebar items
- Build a new about page
- Get lightweight analytics — Cloudflare’s GraphQL API lets you query page-level traffic data without adding any tracking scripts to the site. It’s not Google Analytics, but for seeing which posts are popular relative to each other, it’s more than enough.
With Astro static site generation, it’s just layout markdown, layout+components, and CSS — and that’s exactly what AI coding agents chew through effortlessly.