When Ghosts Fly
I've experimented on-and-off with Ghost over the years. It's been fun to watch it grow from a Kickstarter into a successful, mature, and sophisticated platform for running blogs and newsletters.
It never stuck though, because, for my personal website, I have a firm commitment to self-hosting and having direct access to my data… but I didn't want to self-host Ghost. I knew that Ghost couldn't work on my DreamHost shared server (doesn't play nice with Node), and I knew what it would take to spin up, secure, and configure a capable VPS. There are one-click options for popular VPS providers, but that still leaves you responsible for updates, maintenance, etc.
A little while ago, I became familiar with Fly.io, a platform-as-a-service provider with a focus on an easy, intuitive developer experience and a nice little free tier. Fly.io seemed to provide a possible avenue for hosting Ghost while maintaining filesystem and direct database access. And after reading every "host Ghost on Fly.io" tutorial I could get my hands on, I decided to dive in and see what I could figure out.
In the end, it wasn't nearly as intimidating as I thought it might be, and I'm happy to share some notes on the process. This isn't meant to be a step-by-step guide, but hopefully provides a helpful overview of how the pieces fit together.
flyctl
You can do a lot with Fly via their web dashboard, but the CLI is where the work really happens. Installing flyctl is simple and painless, and linking it to your Fly.io account is also a breeze. One minor annoyance: there's no build of the Fly CLI for Raspberry Pi (which I use as a small dev box). I worked from my Mac instead.
Once you have flyctl
installed, you'll want to launch two machines: one for your Ghost application, and one for the database, as well as volumes (persistent storage) for each machine. Fly will generate a file called fly.toml
for each machine; you can replace those files with the fly.toml files I provide here (application) and here (database).
The differences are relatively small; basically configuring where the volume mounts, ports, options for the MySQL process, and some environment variables. If you've ever worked in Dockerfiles, fly.toml
will feel loosely familiar.
Once you've made your changes to each fly.toml
, flyctl
has a "deploy" command which effectively just syncs your fly.toml
changes with the remote machine. You'll also use this in the future if you need to make updates to the fly.toml
, such as if Ghost releases a new major version and you need to update your Docker image.
There are a few other miscellaneous steps here, such as setting secrets (your MySQL passwords, primarily). My script should be easy to follow if you want to see how I accomplish this.
The short version: launch machines, create volumes, modify fly.toml
config files, set secrets, deploy updated fly.toml
configs. Boom.
Ghost
Fly will provide you with a *.fly.dev
URL by default, and I decided to set up and configure Ghost using my fly.dev URL before I moved to a custom domain. At this point it's as simple as opening yoursite.fly.dev/ghost
in a browser to complete the setup process.
At this point you can customize your theme, set colors, etc. I chose to disable all newsletter related functionality on my site but this is also when you may choose to activate Mailgun for sending emails.
Migration
I spent a good hour or so on migrating content to this site. Most of that time was spent playing with Ghost's official plugin to export my site content. When I first saw the plugin listing, it did jump out that it only has a 3.5 star rating, and I assumed that was review bombing or just caused by general confusion. Unfortunately those 3.5 stars are merited, as the plugin does make some odd choices and comes with some limitations.
The tl;dr: I didn't end up using the export generated by Ghost's plugin. It had too many issues, and in the end I only wanted to migrate 5 posts and 3 pages; copying and pasting was faster and less error prone. (The older stuff is now on a legacy domain, should anyone care to see it.)
And so, here are few unsolicited recommendations to the Ghost team, to improve the plugin export experience:
- Allow users to choose content to migrate by post status. I initially ended up with a massive export due to the dozens (hundreds?) of draft posts I've accumulated over the years, and spent some time manually editing the JSON export to whittle that down.
- Fix the image backup logic. Ghost seems to export the post text data (the post content, metadata, etc) expecting that the image source URLs are in a year/month folder structure, but then creates a folder that places all the images in a single directory. I assumed at first that this was a "me" issue because the documentation explicitly called out that the images should be in the date-based structure, but after reviewing the code, I'm pretty sure this is just an omission, because all images are forced to a specific folder prefix. Weird.
- Corollary: the current image backup uses a recursive directory iterator to bundle up all the files in your uploads directory, ignoring images that don't have any attachment to post or page content. A better approach would be to use post content and metadata to determine which images should be put into the export. This was also an issue because I had some malformed image files (from old tests) that caused my export to completely fail to import the first time I tried uploading it into my Ghost instance. I had to unzip the export, delete those files, then re-zip and re-upload. The Ghost import code could do a better job of handling this too, to be fair.
- Post content importing as HTML is a mistake. Ghost's editor is a major strength of this software, and having old posts import in HTML/code blocks means users can't interact with old content without a massively degraded experience. Instead, parse the content and split it into native Ghost blocks. If you can't easily do that in PHP, improve the import code or provide a migration path on a one-by-one basis.
Launch
After I had the site looking and behaving the way I wanted, Fly makes it easy to provision SSL certificates. Adding your custom domain and generating certificates is a single CLI command (two if you are also supporting a www.
) and then you update your domain's DNS to the appropriate A record (and CNAME, if applicable).
This is one area where I found the Fly.io web dashboard to be more useful than the CLI, because it provides you with very specific instructions for which records to add for both root domains and subdomains. The web UI also offers a special CNAME you can use if you want to have your domain validated before changing your site's A record. (I had a few minutes of site downtime because I didn't see this screen before changing my site's A record, and had to wait for the domain to validate before the certificate could be generated. Not the end of the world for a personal blog, but worth noting if you're doing anything with more significance.)
Live on Ghost
And that's it! The whole process took me less than a few hours, and much of that time was my quixotic migration journey. If you're starting a new blog from scratch, or otherwise avoiding migration, you could probably be up and running in 45 minutes or less. Fly.io's platform is impressive, and I look forward to experimenting with it more.
I'm still working on some improvements as well; namely backups. I plan to use GitHub Actions for this (credit), and have proved out the initial concept with the fly CLI and mysqldump (just need to push the backups somewhere like Dropbox or S3… more to come).
Overall, this was much more pleasant and fun than I expected, and I am excited to breathe new life into this old website, and hopefully contribute back to Ghost as well. As fully featured and mature as the tech is, it still feels like there are a lot of opportunities ahead and new innovations to explore. I'm looking forward to being part of the ride.