The fact that I'm able to write this blog post fills me with joy. It's the first time since starting my blog (which isn't all that long ago) that I got contacted by a reader asking for more information. It's nice to know that someone is actually reading what I write 😉.
Damiano Curia contacted me with regards to Ghost, the blog platform I use to host this site, and running it on Azure. He thinks my blog is beautiful (well, thank you!) and would like to start his own, but has some more questions. So let's get to answering them.
Why isn't it possible to continuously deploy from the official Ghost GitHub repository?
The first question already comes with a multi-layered answer: why you can't do it and why you shouldn't do it.
Why can't you use the official repository?
Azure App Services use Kudu for continuous deployment from Git and has an out of the box connection to GitHub. Kudu only supports deploying from your own repositories in GitHub, not from all publicly available repositories.
This means you'd have to fork the official repository to use with Kudu, and then always keep it in sync with the official repository to get the latest changes.
Why shouldn't you use the official repository?
There are multiple reasons why I wouldn't depend on the official repository to do Continuous Integration and Deployment to my "production" blog.
First, every time they commit something to master, it would trigger an update to my blog instance. While the Ghost team delivers awesome software I don't want any intermediate code ending up on my main blog. The Ghost Release Uploader function downloads the latest release, performs some magic and uploads it to my GitHub repository.
Second, just copy-pasting the files to an Azure App Service won't make your blog run. Ghost expects installations and updates to be done through the Ghost CLI, but that doesn't run so good on Azure. The Ghost Release Uploader does some additional steps to make this process run smooth:
- Add config files with some default settings
- Adds support for Application Insights
- Adds a web.config file that already supports Let's Encrypt challenges
- Adds a Node.js script to update the Ghost database
- Adds a Kudu deploy script that runs npm install and some other commands, to actually start the software
Should I fork your repository or Radoslav's?
Whichever you choose. I started my fork for use with Kudu, with the intention of syncing with Radoslav to keep updated. For reasons unknown, he wasn't able to keep his repository updated so I ended up using (and changing) his Azure Function for keeping my own repository up to date with the Ghost releases.
Not sure if what the big differences between our repositories would be, if any, since he now keeps up with the releases almost immediately after they are posted.
I do remember noticing after several minor Ghost updates that my database wasn't being updated to the latest schema's, so I made some changes to db.js.
No more issues now, and that will probably be biggest difference between repositories.
Where does Ghost store its data?
Ghost stores the data where you configure it to store them, but by default it's the content/ folder in the root of your App Service. This means all your assets (mainly theme files, blog images and the sqlite database) are stored inside the App Service instance itself. Ghost doesn't support multi-instance deployments, so mostly this doesn't pose any issues.
You can choose to externalize your database and instead of using sqlite, you can switch to Azure Database for MySQL. I did this for a while but it provided no added value, except for adding some more euros to my bill.
Ghost has a concept of storage adapters that allow to externalize your images as well. The one for Azure Storage hasn't been updated for over a year and has some issues, but Jessica Deen forked and improved it so I would suggest using that one. One remark: if you think of doing this, do it from the start! Existing images don't get transferred over, so you'll have to go through your old posts and re-upload all the images if you want them on Azure storage.
How do I prevent overriding data when I do a Ghost update?
You don't need to worry, as this is handled by Kudu. Kudu only manipulates data that that was included in a previous deploy. Since your deploy didn't include your assets or your database file (or anything you upload manually to your App Service), nothing gets deleted.
If you make manual changes to files that were included in the automated deploy, these files will get overridden when a new automatic deploy happens.
How can I backup my data?
All the relevant data lives in the content folder, so you can use an Azure Function and the Kudu REST API to periodically download the content folder.
You might run into issues with the database file, it can be locked. Tom Chantler wrote a blogpost to circumvent that issue.
What is your continuous deployment pipeline?
I'll answer this one in two steps, because I'm currently (gradually) changing it to be faster and more resilient.
I have two slots for my App Service, one is running production and the other one is a dev slot. Azure App Service has a Deployment Center to set up the continuous deployment and I used the default Kudu deployment. The default method allows you to setup a different "connection" to GitHub for each slot.
My dev slot was connected to one branch in my GitHub repo (the branch that gets auto updated by the Azure Function) so it would always, automatically, get the newest release. Kudu runs the deployment script (so all the npm install and other actions) in memory of the App Service and that takes a lot of time.
After validation of a working release, I'd merge the main branch into a second branch that was connected to my production slot.
This approach has some downsides:
- npm install is slow and now it has to be run twice (once for dev, second time for production)
- a node app only starts as soon as the first request hits the App Service
- the website is unresponsive for a longer time then I would like
- I need to manually merge from one branch to another
I've got a new pipeline in the works that is using Azure DevOps Pipelines to perform all the tasks. Main difference with a Kudu pipeline is that it is one pipeline for all slots, so no need for two branches anymore.
My new build pipeline runs the following steps:
- Download files from GitHub repository
- Runs npm install to download all necessary packages for Ghost to run
- Trigger the web job from Tom Chantler
- Download the content/data and content/images folder from my production slot
- Extract the production data and images folder into the correct locations on disk
- Rename the database file from backup.db to ghost.db
- Run the database upgrade script
- Delete some extra files
- Create a zip file from all the files and post as a build artifact
I also have a release pipeline that is triggered as soon as a new build is available, it then executes the following steps:
- App Service Deploy to my dev slot
- wget request to the slot to warm up
- After approval (using a predeployment gate), it does a slot swap with production
Still not ideal with the "internalized" images and database but at least my blog won't be unreachable for such a long time as would be the case with the Kudu deployment!