If you ever deployed a blog or website to the public internet, you know that running a live site usually incurs operational costs. For example, a managed blog site hosted on Ghost.org starts at $29/month.

You can considerably cut down such expenses by using a self-hosted solution, but a publicly reachable server is not free. A self-hosted solution typically costs around  $5/month. Even a relatively inexpensive solution that was described in the Ghost CMS on Hetzner Cloud tutorial here on Jamify.org cost you at least $3/month.

As Jamify sites are static sites they can be deployed to a global CDN and do not require traditional servers. CDN deployments are extremely cost efficient, therefore you can benefit from the free tiers of Netlify or other providers and run your live site at no costs on a global CDN.

However, up until recently, you still needed at least a self-hosted Ghost CMS installation on a public server. Why? Because not all images were included in your Gatsby bundle, so they had to be served from your Ghost CMS! That's why you had to make sure your CMS is publicly reachable and available 24/7, rendering all cost benefits of the static site approach useless.

With the new Jamify plugin gatsby-rehype-inline-images you can now integrate all images into your static site bundle, so you do not need the CMS for serving images anymore. This opens up the possibility to deploy your blog from localhost for free.

Ghost CMS on localhost

Besides costs, being able to run your headless Ghost CMS on localhost has a couple of notable advantages:

  • Easier setup as all production concerns fall away.
  • No need to worry about security (firewalls, proxies, ports, authentication, SSL certificates, etc.).
  • You can build your Jamify website offline and achieve fast build times that are not limited by network bandwidth (unless your blog incorporates external images).

Of course, a local CMS installation also has some limitations and cannot cover all use cases:

  • Inviting team members is not possible (no public access).
  • Member functions (e.g. newsletter subscription and distribution) cannot be used (no publicly available backend).

Even if you need a public Ghost CMS installation at some point in time, a localhost installation is still the number one choice in a lot of testing scenarios.

Local CMS install

Let's start with installing a Ghost CMS on your local machine. The following instructions have been tested on a Linux system running on Fedora, but should work on other Linux flavors too, including MacOS.

If you are using Windows it is recommended to install the Windows Subsystem for Linux (WSL 2) and use Ubuntu 20.04 LTS.


To install Ghost locally make sure you install  Node.js, the package manager yarn and the ghost-cli. Even if Node.js is already installed, check that you have a recent version:

$ node -v

Node comes bundled with the package manager npm. Make use of it right away and install yarn and the ghost-cli with it:

$ sudo npm -g install yarn ghost-cli@latest

Install Ghost

With the needed tools available, you can now create a new directory and install Ghost CMS in it:

$ mkdir ghost-local
$ cd ghost-local

Use the previously installed ghost-cli for performing the actual install:

[ghost-local]$ ghost install local

It may take a minute to download and install all files. After a successful install, you can visit the CMS under http://localhost:2368/ghost/ and complete the setup process in three simple steps:

For a local install, you can safely enter a bogus email address and skip the staff user invite as you won't be able to work in teams anyway. After completing the final step, you should be brought to the Ghost Admin dashboard.

Create API keys

The initial setup is now complete. You can make further customizations in the admin interface, but this is not required at this point in time.

In other tutorials on Jamify.org you may read the recommendation to turn on headless mode. For a local installation this is not needed as the installation will never be available publicly, so you can safely skip this step.

In order to be able to connect to your Ghost instance with Jamify later, you need to generate a content API key. Go to Integrations -> Add custom integration

and click Create in order to generate a new key:

Starting and Stopping

The installation routine automatically starts the local Ghost server. You can stop it with the following command:

[ghost-local]$ ghost stop

You will also have to restart your server after every reboot:

[ghost-local]$ ghost start

If you want to check whether or not Ghost is currently running you can use this handy command:

$ ghost ls

Keep the current Ghost server running if you want to follow along with this tutorial.

Importing Content

If you are already running another Ghost instance, you may want to import your content to your local installation. The easiest way is to use the import/export functionality under the Labs section.

Unfortunately, images must be transferred manually. So, in addition to the above import/export for the textual content, copy all images in directory content/images/ from the source to the target Ghost instance.

Local Jamify install

With a local CMS install it also makes sense to generate your static site locally, in fact, you won't be able to build your site with a cloud provider because your localhost CMS is unreachable for them. Let's download the Jamify starter:

$ git clone https://github.com/styxlab/gatsby-starter-try-ghost.git jamify-starter

and change into the work directory:

$ cd jamify-starter

Adding Keys

The Jamify starter must be told to source in the CMS content from you Ghost CMS on localhost. For that, create a new file called .ghost.json in your work directory and copy the previously generated content API keys in it:

  "development": {
    "apiUrl": "http://localhost:2368",
    "contentApiKey": "2a087eea8fc3c9a3e7392625c0"
  "production": {
    "apiUrl": "http://localhost:2368",
    "contentApiKey": "2a087eea8fc3c9a3e7392625c0"
Don't forget to replace the contentApiKey with your own!

Remove members plugin

The Jamify starter includes some plugins for convenience, one of which is the gatsby-theme-ghost-members plugin. As discussed earlier, the members plugin needs a public backend which currently must be a Ghost CMS, therefore the subscription flow won't work with a CMS on localhost. That's why you should remove this plugin:

[jamify-starter]$ yarn remove gatsby-theme-ghost-members

and also exclude it in your gatsby-config.js:

// gatsby-config.js

plugins: [
    //    resolve: `gatsby-theme-ghost-members`,
The gatsby-theme-ghost-members plugin could be enhanced to work with other backends or connect with serverless functions. If you want to contribute to this project, you are more than welcome to. Just let me know.

Add plugin for inline images

While feature and all meta images are included in the static bundle by default, all inline images within posts or pages are only included if you add the new gatsby-rehype-inline-images plugin:

[jamify-starter]$ yarn add gatsby-rehype-inline-images

This is a sub-plugin of the gatsby-transformer-rehype plugin and must be placed into your gatsby-config.js as follows:

// gatsby-config.js

plugins: [
        resolve: `gatsby-transformer-rehype`,
        options: {
            filter: node => (
                node.internal.type === `GhostPost` ||
                node.internal.type === `GhostPage`
            plugins: [
                    resolve: `gatsby-rehype-ghost-links`,
                    resolve: `gatsby-rehype-prismjs`,
                    resolve: `gatsby-rehype-inline-images`,

Including this plugin may result in longer build times as all inline images must be downloaded. However, as you are downloading them from localhost you are not limited by network bandwidth.

The gatsby-rehype-inline-images plugin also processes images, so they are lazy loaded and fade in with a nice blur-up effect known from Medium. See the plugin readme to learn more about the options this plugin provides.

Smoke test

You can now start your first test build with

[jamify-starter]$ yarn develop

and see if the build succeeds. Check the results on http://localhost:8000/. Next, add a new article in your Ghost CMS on localhost and press the Publish button. After that rebuild your project again:

[jamify-starter]$ yarn develop

and verify that the new article comes through:

Build your static site

While the development command is great for testing, you need to issue the build command to generate an optimized bundle that can be deployed to a CDN:

[jamify-starter]$ yarn build

Check that building succeeds without errors and start the build server:

[jamify-starter]$ gatsby serve

Visit your site at http://localhost:9000/ and verify that it looks exactly like in development mode.

Deploy to Netlify

Now its time to deploy your site to a global content delivery network (CDN). This step has been described in more detail in the getting started tutorial.

Getting started with Jamify
Proudly introducing Jamify: A modern tool-set for publishing flaring fast blogs with Gatsby and Ghost. Start here to learn how Jamify enables you to deliver content at stellar speeds and why it makes your site more secure. Surely, you’ll love its amazing flexibility. Join the Jamstack hype here!

Here, I just repeat the simple steps. Once you have the Netlify CLI tool installed

$ sudo npm -g install netlify-cli

you can login with

[jamify-starter]$ netlify login

and deploy the previously build bundle from the public/ folder.

[jamify-starter]$ netlify deploy --prod

Answer the upcoming questions as follows:

Your site is manually deployed to Netlify, which means the generated bundle is uploaded and published.

This example was shortly available under  https://eager-golick-336605.netlify.app. Yours should be on another subdomain. You can get a nicer subdomain, if you specify a site name above, however, it must be one that is currently not in use.

Congrats! 🎉 Believe it or not, your site is now running on a flaring fast CDN with zero operational costs. Plus, your site is SSL secured and enjoys all the Jamstack security benefits.

Custom Domain Names

Although optional, many people want to deploy their blog on their own custom domain. You can either purchase a new domain on Netlify or bring your own. A custom domain name will typically cost you around $1,5/month.

If you are a student, here is how you can get a free custom domain name! Sign up for GitHub Education. You may also get free custom domain names from name.com or namecheap.com.

Continuous Deployment

In another tutorial article, I showed how to set up a continuous deployment pipeline, where a content change in your CMS triggers a site rebuild which is then automatically propagated to your live site on the global CDN.

Continuous content publishing
Setup a continuous deployment pipeline, so your content gets automatically published on a global content delivery network. This tutorial explains how you achieve full automation, so any update to your content will propagate smoothly to your public site.

This setup heavily relies on online cloud services: your CMS must have a public endpoint that triggers a webhook on Gatsby Cloud which deploys the build to Netlify. With a CMS on localhost, this setup won't work anymore.

Webhooks on localhost

With a neat webhook open-source project from Adnan Hajdarević you can create a similar pipeline on localhost too. There are different methods available for installing this program, here I choose to download the webhook-linux-amd64.tar.gz binary package, unpack the file and copy the executable in /usr/bin/ folder of my machine to make it globally available.

The webhook program consumes a hooks.json definition file and spins-up a server on localhost. We need to define two actions:

  • Build the jamify-starter with yarn build
  • Deploy the build package with netlify deploy --prod

which can be combined in one script file called build-and-deploy.sh that you should place in your work jamify-starter directory.


yarn clean
yarn build
netlify deploy --prod

Make this file executable with:

[jamify-starter]$ chmod +x build-and-deploy.sh

You can now create the hooks.json in the same directory:

    "id": "jamify-webhook",
    "execute-command": "./build-and-deploy.sh",
    "command-working-directory": "/home/styxlab/jamify-starter"

The working directory must be an absolute path, please change it to your own. You can now spin-up the webhook server:

[jamify-starter]$ webhook -hooks hooks.json -verbose -port 7000

Configuration is now complete. Test it out by triggering the webhook with curl

$ curl -X POST

Connect to CMS

You can add the webhook easily to your CMS on localhost, so it get's triggered whenever you make changes to your content:

Test your pipeline

Let's make a simple content change. For this test, I change the title of the first post and also exchange the feature image with another one from Unsplash. Make sure to hit the Update button after making these changes.

After the build has completed, your live site should then be automatically updated with the new content changes.


If you do not need member functions and mostly write your articles yourself, the described setup may be exactly what you need for running an up-to-date, super-fast, SSL-secured, shell-proof public blog site with 99.99% uptime at zero costs 🥳.

While this article focuses on the operating costs of a live blog site, it is worth mentioning that this solution makes blogging not only more affordable, it also makes blogging more accessible: The install process is much simpler, so that it is feasible for people who feel discouraged by traditional self-hosting solutions.

Furthermore, taking a public CMS server out of the equation will certainly reduce complexity, ongoing maintenance efforts and let your mind put at easy: without a server, you cannot be hacked.

Missing a tutorial on Jamify? Open a new tutorial issue and consider contributing one yourself. We are a friendly open-source community and are looking forward to welcoming you!