From WordPress to Ghost: Migrating The Dougie Chronicles on Kubernetes
How I replaced WordPress with Ghost CMS on my Talos Linux Kubernetes cluster — Ansible playbooks, a custom Handlebars theme, Resend for email, and why Ghost is simply a better fit for a homelab blog.
WordPress is a capable platform — it powers a huge chunk of the web for good reason. But for a homelab blog, it can feel like more than you actually need. The plugin ecosystem is vast, themes are flexible, but the overhead of keeping everything updated and configured adds up. I ran WordPress here for a while and it worked fine. I just started wondering if something more focused might suit this kind of site better.
Ghost CMS turned out to be the answer. Here's how the migration happened and what the stack looks like today.
Why Ghost
Ghost is a publishing platform built specifically for content — posts, pages, tags, authors, memberships, newsletters, and a solid API. It's purpose-built rather than general-purpose, and that focus shows in everything from the editor to the admin interface. It also feels snappier than WordPress, both in the admin and on the public-facing site.
For a homelab blog, it's simply a better fit.
The Old Stack
The WordPress deployment was an Ansible role driven by a top-level playbook:
---
# Deploy thedougie WordPress (www.thedougie.com)
# Usage: ansible-playbook playbooks/16-deploy-thedougie-wordpress.yml
#
# Requires vault secrets (tag: lenux-talos1):
# thedougie_db_password — MySQL wordpress user password
# thedougie_db_root_password — MySQL root password
#
# What this deploys:
# - MySQL 8.0 (single instance, Longhorn RWO PVC)
# - WordPress php8.3-apache (Longhorn RWX PVC for shared /var/www/html)
# - Gateway API HTTPRoute → https://www.thedougie.com
- name: Deploy thedougie WordPress
hosts: localhost
connection: local
gather_facts: false
vars_files:
- ../group_vars/talos_cluster.yml
roles:
- role: vault
- role: thedougie-wordpressIt worked well. MySQL backend, WordPress sharing a Longhorn RWX volume for /var/www/html, routed through Gateway API. The infrastructure side was solid — the switch to Ghost was more about finding a platform that fit the workflow better.
The New Stack
The Ghost deployment follows the same pattern — an Ansible role called from a playbook — but what the role actually deploys is leaner:
---
# Deploy thedougie Ghost CMS (www.thedougie.com)
# Usage: ansible-playbook playbooks/25-deploy-thedougie-ghost.yml
#
# Requires vault secrets (tag: lenux-talos1):
# ghost_db_password — MySQL 'ghost' user password
# ghost_db_root_password — MySQL root password
# ghost_resend_api_key — Resend API key for transactional email
#
# What this deploys:
# - MySQL 8.0 (StatefulSet, Longhorn RWO PVC)
# - Ghost (Longhorn RWX PVC for /var/lib/ghost/content)
# - Gateway API HTTPRoute → https://www.thedougie.com
# - Gateway API HTTPRoute → thedougie.com apex (301 redirect to www)
- name: Deploy thedougie Ghost CMS
hosts: localhost
connection: local
gather_facts: false
vars_files:
- ../group_vars/talos_cluster.yml
roles:
- role: vault
- role: thedougie-ghostThe role deploys in sequence: namespace, Kubernetes Secret with vault-injected credentials, MySQL StatefulSet on a Longhorn RWO PVC, Ghost content PVC (Longhorn RWX for /var/lib/ghost/content), the Ghost Deployment itself, a ClusterIP Service, and finally the Gateway API HTTPRoutes for both the www subdomain and the apex redirect.
Config via Environment Variables
Ghost reads all its configuration from environment variables natively — no config file or ConfigMap needed. Database connection, URL, mail transport, and proxy trust are all set as env vars on the container. This keeps the deployment simple and makes configuration changes a one-line edit in the role defaults, followed by a playbook re-run.
Secrets Management
Credentials are stored in a secrets vault and injected at deploy time. The Ansible role pulls them in during the play and writes a Kubernetes Secret that the Ghost and MySQL containers reference via secretKeyRef. Nothing sensitive lives in the repository.
- name: Create ghost-secrets Secret
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: ghost-secrets
namespace: "{{ ghost_namespace }}"
type: Opaque
stringData:
MYSQL_PASSWORD: "{{ ghost_db_password }}"
MYSQL_ROOT_PASSWORD: "{{ ghost_db_root_password }}"
RESEND_API_KEY: "{{ ghost_resend_api_key }}"Email with Resend
Ghost needs a mail transport for two things: transactional email (password resets, member notifications) and newsletter delivery. Both are handled through the same SMTP configuration pointing at Resend.
Ghost also has a native Mailgun integration for bulk newsletter sending, but it's optional. When Mailgun isn't configured, Ghost falls back to the SMTP transport for newsletter delivery as well. With a small subscriber list, the SMTP path through a solid deliverability provider like Resend is perfectly adequate — and it means one less external service to configure and maintain.
Building a Custom Theme
One of the biggest wins from switching to Ghost is how much easier it is to build and maintain a custom theme. Ghost's theme system is based on Handlebars — a lightweight templating language that lets you write plain HTML with simple placeholders for dynamic content like post titles, body text, author names, and tags. There's no heavy framework or proprietary block system to learn. You define the structure of your pages in template files, drop in Ghost's helper tags where you want content to appear, and the result is exactly what you'd expect.
The system is organized and predictable enough that you can build something genuinely custom without a lot of scaffolding. Partials make it easy to reuse components like headers, footers, and post cards across templates without duplication.
For the theme here, what I wanted was a clean design that matched my vision for the site — how posts are laid out, how tags and topics are surfaced, the overall look and feel. Dark-mode-first, card-based, and actually reflective of the homelab content rather than a generic blog aesthetic.
Was It Worth It?
Yes. The migration took an afternoon to get the Ansible role sorted and the Ghost deployment running cleanly. Getting the theme where I wanted it took longer, but that time was spent on actual design decisions rather than plumbing.
The result is a site that feels faster, is easier to maintain, and is easier to extend. The Ghost admin is something I actually enjoy using. The theme system means I know exactly how every part of the site works and can change any of it quickly. And the Ansible role means the whole thing can be torn down and rebuilt from scratch with a single playbook run — credentials and all, injected from the vault at deploy time.
If you're running a homelab blog and looking for something a bit more focused, Ghost is worth a serious look.