Archived Content
This site now runs on Ghost in Azure hosted for free1. Yes, this blog has officially become nothing but me switching blog engines and then writing about it, but hey, maybe someone finds it useful? š¤·
tl;dr
- Make a small change to ghost source code (for iisnode)
- Use Azure App Service free tier to host your blog
- Setup an Azure CDN with a custom domain and SSL to act as a reverse proxy
- Enjoy static site performance on dynamically generated content
Background
I used to run this site on Ghost a few years ago when it first came out. I switched it to use a static site generator shortly after that mostly because I thought it would be a fun little project (also performance and security and all that nonsense).
Why the Switch Back?
I guess my mistake was I never stopped following @TryGhost on twitter and they recently started tweeting about all the new features they were adding. I got a little jealous.
Also, despite all the benefits of a static site, a major drawback of that approach is the barrier to writing something. In order to write something, I usually fired up my editor on one of my machines where I had the repo already cloned. If I was on a strange machine or on a phone, forget about it. Yes, there are ways to make it easier to edit your static site in theory. In practice, I never did those things and hence I wrote less even though Iāve had ideas that I have wanted to flesh out here.
With Ghost, Iām writing this post from my phone as I watch my one year old play in his room (this is my life now). I probably never would have started writing this post otherwise if I wouldnāt have been able to do that.
Ghost on Azure App Service
Felix Rieseberg has a repo where he setup a one-click deployment to setup a Ghost blog on Azure. The only problem is that it hasnāt been updated for Ghost 1.0 yet. It is still using v0.11.9 as of this writing and Ghost 1.0 has all the new hotness.
I decided to give it a go to see if I could update it to make it work. If you want to see all of the changes I made in one place, checkout my ghost repo on github.
CLI Tool
I want to mention the new CLI tool that Ghost has introduced to setup a blog automagically. Unfortunately, I havenāt found a way to reliably use it on Azureās app service. Also, I donāt think it would work with the code modifications that are required. So, I didnāt try to use it any further than that. But, supposedly, I guess if you have full access to a server, it makes it easier to setupā¦?
New Configuration System
Ghost 1.0 has a completely new configuration system which presents a problem with the way Felix configured Ghost pre-1.0. Ironically, the new config system is supposed to make it easier to support more configuration sources (e.g. env variables, json files, etc.), but in this particular case, it has made it harder.
When you host a node app in Azure, it will run through iisnode (at least on Windows - I havenāt tried any of the new native Linux stuff yet which may be easier). iisnode passes the port it wants the node app to listen on as an environment variable called PORT
. Seems pretty straightforward.
Previously, Ghost just allowed you to put a config.js
file in the root of your site and you could use any arbitrary javascript to configure the app. Now, you canāt do that. You have to either pass an env variable called SERVER_PORT
or configure the port in a json
file like this:
Long story short, I had to modify core/server/config/index.js
to pull the port specifically from the PORT
env variable:
I donāt know if that is the best or easiest place to make that change, but it is what I did, and it works.
The only other thing I had to do was add an iisnode.yml
and a web.config
file with the standard stuff in it for hosting a node app. I also included some HSTS stuff in the web.config
since I only ever want to serve over HTTPS. See the repo for details.
Creating the Ghost Database
Pre-1.0 Ghost used to create a SQLite database for you automagically if it didnāt find one when first starting up. This made it incredibly easy to get started with the product, but had its limitations. With the introduction of the new CLI tool, the CLI is the thing creating the database now. And like I said earlier, I couldnāt run that on Azure App Service.
I ended up having to run the knex-migrator (which is what the Ghost CLI uses internally) manually on my machine to create the database locally and then FTP it up to Azure. I created a db.js
file in the root:
And then, after npm install
:
This assumes you setup a config.development.json
and/or a config.production.json
in the root with the database you want. For me, Iām still using SQLite:
I briefly played around with trying to get everything to work with the new MySQL-in-app with Azure App Service, but you canāt access that database remotely and I couldnāt get any of the CLI tools running in the Azure Console. Also, I remembered that I donāt care and SQLite is fine.
Deploying to Azure
Once you login to your free Azure account, you will want to click on App Services:
And then from there click on + Add and choose a Web App:
You can then choose your app name and make sure to choose the Free Plan:
As of this writing, Azure defaults the node version that will run your app to some old-ass version. Youāll need to update it to the latest LTS version so that Ghost will run properly. Click on your new app service and then on Application Settings. Scroll down and make sure the WEBSITE_NODE_DEFAULT_VERSION
is set to 8.9.4
(the latest LTS at the time of this writing):
With the correct node version in place, youāll next want to setup deployment for your site. I like using the Github integration so that anytime I push to a particular branch, it will automatically redeploy the site. It will also npm install
any dependencies during deployment.
Navigate to Deployment Options to choose your source:
From there, I chose Github and filled in all my settings:
I set my repo up with a few branches for easier maintenance. The ghost
branch is where I commit the Ghost releases as they come out unchanged. The azure
branch is where I made only the changes necessary to get it running on Azure. I rebase this branch off of ghost
as new releases come out. I then create a separate branch for each site I deploy. e.g. I created a chadlynet
branch for this site and I create other branches for family members that want their own sites. This way I can make theme customizations and tweaks to individual sites and rebase those changes off of the azure
branch.
Settings
Once your site is deployed, you will want to set some environment variables. You can set any of these options, but the main one we want to set is url
. Click on Application Settings under your app service and then scroll down to App Settings. You will want to set url
(casing matters) to the URL of your site:
Make sure to Save after setting that setting.
Info
You donāt need to set the
NODE_ENV
as iisnode will set that toproduction
for you automatically.
A Working Site
You should now have a working site at yourname.azurewebsites.net. You can start to set your site up now by logging in at /ghost. A limitation of the free tier is that you canāt set a custom domain. You are forced to use the azurewebsites.net domain. We shall get around that limitation by using Azure CDN and get some significant performance benefits along with it.
Azure CDN as a Reverse Proxy
Technically, the Azure CDN isnāt free. But it is so cheap, that it is practically free. For the standard tier, as of this writing, it is $0.087 / GB. For my site, since I am only serving text and small images, it ends up costing me on average a little less than $0.07 / month. Trust me, your shitty blog, like mine, will also not be expensive. š
Static Site Performance
A site like mine (one that doesnāt get updated very often) can really benefit from being served like a static site e.g. almost always from cache. If you set the cache headers right in Ghost, you can get this benefit with Azure CDN since it will act as a caching reverse proxy honoring the cache headers from your site.
To setup the caching config for Ghost, you will need to edit the config.production.json
file in the root:
This tells Ghost to cache the frontend of the site (e.g. not the admin section at /ghost) at 72,000 seconds = 20 hours. By default, Ghost will set this number to zero. You will want to bump that number to whatever you are most comfortable with. The way I have it setup, when I write or edit a post, it may or may not show up for people depending on where they are for up to 20 hours. I am OK with that for the performance benefits it brings.
There is a little more nuance to this concerning ETags and things; more on that in another post.
Setup
To setup the CDN, you will want to click on the + New icon and search for / choose CDN:
From there, fill in your settings making sure to choose Standard Verizon as the pricing tier. As of this writing, Verizon is the only provider that supports SSL on custom domains. This is supposedly ācoming soonā to Akamai, but it is not here yet.
Youāll also want to choose Web App as the origin type and choose your existing web app you created previously. You can include the CDN in the same resource group as your web app to make it easier to manage going forward.
After you create your CDN, it takes Verizon 6 hours or so to get off their ass and actually propagate your changes through their network. Until then, your new endpoint whatever.azureedge.net will return 404
s. Donāt fret, just relax. Go read a book or something (it is like a movie but on paper).
If you arenāt into books, while you wait, you can also setup your custom domain. In your CDN profile, click on Endpoints and then choose the existing endpoint we just setup. From there, click on + Custom Domain. Youāll have to setup a CNAME
to whatever.azureedge.net for your domain.
Again, the domain takes way longer to propagate than you think it should. While you are in there, you might as well take advantage of the free SSL that you can get with your domain. Click the custom domain you setup and enable HTTPS:
They will send an email to a bunch of different addresses associated with your domain. Click the link on one of those emails to confirm and setup the SSL.
Once that is done, it will take some time to propagate throughout the CDN network.
Update App Settings
Once you have all of this setup, your site will be exposed on 3 domains, yoursite.azurewebsites.net (the web app), yoursite.azureedge.net (the CDN endpoint), and your custom domain. Your custom domain is obviously the one you count as your site, e.g. the canonical version.
If you are worried about search engine penalties for duplicate content, donāt be. Ghost will autogenerate a canonical link element on each page of your site using the url
configured in your web appās App Settings (which we setup earlier). You will want to update that URL now to your custom domain with HTTPS. And, yes, canonical link elements work cross-domain.
Warning
You explicitly donāt want to setup
301
redirects from your web app to the canonical domain. The CDN reverse proxy would then not be able to access the web app. Just think of the azurewebsites.net version of your site as the non-cached version of your site where you can see your changes before the rest of the world sees them on your ārealā site.
Final Product
Once I had all of this setup for this site, I decided to perform an experiment. I went to the public facing homepage to prime the CDN cache. I then went into the Azure portal and stopped the underlying app service. So, effectively, this site at the azurewebistes.net domain started returning 503
s and became unavailable. I then refreshed the page on the public facing (CDN) site and everything still worked fine. Then, just to be sure, I opened dev tools and disabled the client cache and refreshed the page again. The server returned a string of beautiful 200
s like nothing was wrong.
In other words, when everything is running normally, even if this site gets slammed, very little traffic will make it through to the actual Ghost app. The global CDN will relieve all that pressure from the web app and keep the site loading fast for everyone.
I dare somebody to DDOS this site.2