I recently changed my Ghost hosting setup just a little to harness a couple of benefits of Ghost(Pro) but still keep some of the levels of control that I want. This is my new setup.


My Ghost setup was originally self hosted on a Droplet (VPS) provided by DigitalOcean (referral link). Being self hosted I have complete control over, well, everything! I run a custom built version of Nginx, bleeding edge OpenSSL, the latest and greatest cipher suites and any security headers I like. My blog is kind of like my playground and whist I enjoy most of what I do here from a technical perspective and learn a great deal, managing and maintaining my Ghost install wasn't one of them.


Ghost offer a hosted solution and the pricing is reasonable but the cost wasn't really my concern.


If you use Ghost(Pro) and want HTTPS with a custom domain you have to wrap it with Cloudflare and their Flexible SSL offering. Whilst you can run TLS to the origin, you can't do strict certificate validation, which makes me uneasy, and I lose various others things too. No more custom cipher suites, security headers and various other things. Yes I miss out on the power of a CDN like Cloudflare but I still want to be able to do my own cool stuff.


In the end I decided to go for a combination of both offerings. Depending on the traffic levels for a given month I was going to need at least the $39/month option for Ghost(Pro). To try and cut that down though and keep costs a little lower I'm going to continue to use my VPS as a reverse proxy. I recently wrote about how I use Nginx to act as a reverse proxy with caching so instead of proxying to a local instance of Node it will now proxy to the hosted Ghost(Pro) instance instead. This means I can easily get away with the Personal tier on Ghost(Pro) by preventing a majority of the traffic hitting the origin, I can still tinker around with all the custom bits I like and I no longer need to worry about screwing up Ghost updates... Also because I know I'm proxying to a different host on the back I can do strict validation of the certificate to the origin and maintain a fully secured TLS connection end-to-end.


There were a couple of other things to consider and they were that my blog would now be available at the Ghost(Pro) domain as well as my own domain. I can't redirect it because I'm proxying so I loaded a piece of script to handle the redirect where needed.

if(window.location.hostname == 'scotthelme. ghost.io')
    window.location.replace("https://scotthelme.co.uk" + window.location.pathname);

I had to add a space to the Ghost(Pro) domain in the above example because of the next step. A lot of the links provided in the now hosted blog point back to the ghost.io domain and I don't want unecessary redirects in there and it'd probably be bad for SEO too. I use a sub_filter in Nginx to handle this for me and replace any instance of scotthelme. ghost.io with scotthelme.co.uk in proxied pages.

proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter 'scotthelme. ghost.io' 'scotthelme.co.uk';

I set the Accept-Encoding header to an empty string so that the upstream won't gzip or otherwise compress the response as this would prevent the sub_filter from matching anything in the response. It is slightly less efficient yes but I'm only hitting the origin once to fetch a page and then serving it compressed from my server with gzip or Brotli where possible. The last thing to sort out was my Disqus integration which for some unknown reason was not happy at all. I think it was because the post ID values changed during the migration so I used their URL Mapper migration tool to fix the problem the easy way. Most of my old comment threads were pinned against the preview URL provided by ghost so I grabbed an export of all of my pages with comments and translated them all to the published post URL. For example the URL https://scotthelme.co.uk/p/7237bd39-2b5c-4f11-8f4b-05dbbdc6346b/ is the preview URL for this article which eventually will map to https://scotthelme.co.uk/moving-to-ghost-pro/ once I publish. This script will take the former, find the latter and output them in the format Disqus wants.

    $fileName = $argv[1];
    $file = fopen("$fileName", "r");
    while(($line = fgets($file)) !== false) {

    function scan($address) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_URL, $address);
        $response = curl_exec($ch);
        $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        echo $address . ", " . $effectiveUrl . "\r\n";

With that done and the new map uploaded to Disqus all of my comments were back and it was ready for action.