Moving this blog from Ghost to Hugo
This blog needed some attention. The old setup was Ghost running on a DigitalOcean Droplet, with a database, nginx, and a CentOS 7 installation that was well past its useful life. The bill was about $12 a month. Not terrible, but keeping an entire server around for some articles and pictures was becoming more work than I wanted.
My main concern was the content. There are years of projects, reviews, and random observations here. I wanted to keep those without carrying the old server along with them.
So, we’re moving to Hugo on DigitalOcean App Platform. Here’s the route I took.
Back up first
Before changing anything, I made a backup of the database and the Ghost deployment files, including the content and images. That archive was copied back to my computer and checked for integrity. It came in just under 1GB. Small enough to keep a complete copy instead of trying to guess which files might be useful later.
One thing worth checking: the latest database had a published article that was missing from the old JSON export. Using that export alone would have left something behind. The database ended up being the source for the migration.
The inventory was 19 published posts, two pages, and five drafts. The drafts needed to stay drafts. Good enough to proceed!
Why Hugo?
Hugo is an open-source static site generator. It takes content and templates and builds the HTML files that visitors actually read. For this blog, that means the public site doesn’t need a running Ghost process or a database.
I wanted fewer things to maintain, and this fits pretty well. DigitalOcean still hosts the site, but App Platform handles the hosting infrastructure. There isn’t a Droplet operating system for me to patch anymore. This is a static site deployment; I didn’t need to add functions or an application container just to serve articles.
There is a tradeoff. I lose the Ghost editing interface. New posts are files in a Git repository, and publishing means committing and pushing a change. For my purposes, that’s fine. Someone who wants to write everything in a browser might prefer a different setup.
Cost was part of the decision too. At the time of this move, DigitalOcean’s static site pricing includes a free tier with a transfer allowance and charges for additional bandwidth. This deployment uses one static site, with no paid application container or database. That puts the base hosting cost below the old $12 monthly bill, though bandwidth and domain registration still need to be considered.
Less server maintenance
The security improvement is pretty practical. Once the old Droplet is retired, there is no longer an end-of-life OS, SSH service, Ghost admin interface, or database for me to maintain on that server. The public blog serves generated files, which removes a lot of the moving parts that could otherwise need patching or troubleshooting. App Platform handles the hosting infrastructure and managed HTTPS, while Git gives me a history of content and configuration changes that I can review or revert and redeploy. That doesn’t make the site maintenance-free. I still need to protect my GitHub and DigitalOcean accounts, keep API credentials out of the repository, review build-tool updates, and keep independent backups. But that’s a much smaller job than looking after the whole machine. Works for me!
Bring the content along
The old articles were migrated with their existing HTML bodies. New articles, including this one, can use Markdown. Hugo handles both, so there wasn’t much reason to rewrite years of old formatting just to make the source files look consistent.
The important parts were keeping the article addresses, dates, tags, and images intact. Existing links should still land on the same posts. The migration copied 272 referenced image files, including the resized versions used by some of the older articles.
While reviewing the content, I also found unrelated shopping paragraphs and a gambling link mixed into old posts. Those were removed from the migrated copies. The original backup was left untouched. No reason to bring that junk along.
The repository is private because it contains unpublished drafts. The generated site excludes those drafts, and the original database and server configuration stay outside the repository entirely. A private repository doesn’t make a published website private, so checking the actual build matters.
Build and deploy
The new flow is pretty straightforward:
- Write or edit a post locally.
- Preview it with Hugo.
- Commit and push to GitHub.
- Let App Platform build and publish the site.
For a local preview, the command is simply:
hugo server
The production build uses the real domain so article links, the RSS feed, and the sitemap all point to the same place:
hugo --gc --baseURL "https://bradsturkie.com/"
Before switching DNS, I checked the migrated pages, local links, images, search, and the missing-page response. The five drafts were also checked to make sure they weren’t showing up in the public output. Loading the homepage is a start, but it doesn’t tell you whether an old project article still has its pictures.
DNS has one more surprise
My domain is registered at Namecheap, but DNS was already hosted at DigitalOcean. Those are two different jobs: the registrar keeps the domain registration, while the authoritative DNS provider serves its records. App Platform was configured for the main domain, with www and the temporary App Platform address redirecting to it.
Did I need to add something at Namecheap? In this case, no. The domain was already delegated to ns1.digitalocean.com, ns2.digitalocean.com, and ns3.digitalocean.com. With that delegation in place, the DNS changes belong at DigitalOcean. Adding a record to Namecheap’s unused DNS service wouldn’t help. If you’re following along, check which nameservers your domain actually uses before editing anything. Don’t switch nameservers without first accounting for existing records, especially email.
With the DigitalOcean-managed domain configuration, App Platform handles the DNS records and certificate provisioning. It wasn’t asking me to copy a validation token or add a TXT record at Namecheap. If you use another DNS provider, follow the records shown for your own app instead. DigitalOcean documents the options in its App Platform domain guide.
There was one wrinkle: an old, separate DNS zone for www was still pointing at the Droplet. That zone took precedence over the new alias in the main zone. After saving its records, I removed the obsolete zone and the new alias started resolving correctly.
Then came the waiting. DNS caches and certificate provisioning don’t necessarily finish at the same time. During this cutover, the www certificate became active while the main domain still showed “pending ownership validation.” That status alone didn’t mean there was a missing registrar setting. App Platform’s status message advised checking the DNS configuration if validation persisted for more than an hour; that is a troubleshooting checkpoint, not a promise that issuance always finishes within an hour.
For a quick check, these commands compare the authoritative server with a public resolver and then test HTTPS:
dig @ns1.digitalocean.com bradsturkie.com A +short
dig @1.1.1.1 bradsturkie.com A +short
curl -I https://bradsturkie.com/
Replace my domain with yours, and use your own authoritative nameserver. My old DNS record had a 30-minute TTL, so some resolvers continued returning the Droplet address after others had updated. Also check www separately. A successful DNS lookup doesn’t prove that the certificate is ready, and a response from a cached old address doesn’t prove the new site works. Leave certificate verification enabled when testing HTTPS.
The retirement sequence was to verify HTTPS and the migrated content on the real domain, then delete the old Droplet. Both certificates eventually became active without any changes at Namecheap. The final checks passed for the published pages, redirects, and all 274 site assets, and the old Droplet has now been deleted. The local backup stays put.
New name, same stuff
I also changed the name from “Sports. Tech. Stuff.” to “Stuff on the Internet.” That seems to cover what ends up here pretty well.
The goal was to keep the articles, simplify publishing, and spend less time maintaining the machine underneath it all. Hugo and a static deployment should do that job nicely. Now, back to writing about the projects themselves!