Deploy Gatsby Site To Your Own Server Using Github Actions

Learn how to deploy your Gatsby generated site to your own self-hosted web server using Github Actions.

Using Github Actions To Deploy Gatsby Site To Own Server Featured Image
Reading Time: 12 minutes

Why Would You Want To?

I should start by saying that there are plenty of fabulous hosting options for hosting Gatsby generated websites using all sorts of automated scripts and such like, including Github integration available from commercial hosts such as Netlify, Fastly, Vercel, Azure and of course Gatsby Cloud themselves.

I’ve tried Netlify and Gatsby Cloud and both are excellent. But neither cater for my particular scenario which is a small set of niche websites from which I make a few bucks (not much – don’t get excited!) because they both stipulate that their free hosting is for not for profit organisations or hobbyists. I am, by definition not either of those.

Now don’t get me wrong, the price point of Netlify or Gatsby Cloud isn’t that bad at around $20/month. But I can have a VPS server at VULTR for $5/month and do other things with it as well as host my Gatsby generated sites.

Hence why I decided I wanted to automate the workflow of building and deploying my Gatsby sites, because until now I’ve been doing it manually using rsync.

Prerequisites

This post is likely to be long, so the first prerequisite will be patience. But you can skip using the Table of Contents above to find the relevant section you’re struggling with. If you’re like me, you struggled with most of it because until yesterday I had never heard of Github Actions and had absolutely no idea how they all stitched together.

Other prerequisites include;

  • Your Gatsby code must be in a Github repository. This can be private or public though.
  • The server you wish to deploy to must be accessible to you by SSH.
  • If using WordPress as a Headless CMS feed for Gatsby, you’ll need to install the GatsbyJS WordPress plugin.
  • You’ll need a basic understanding of YAML to work out the GitHub Actions.

Did I mention you may need patience.

Put Your Gatsby Code Into A Github Repository

The easiest way to do this is to start with your Gatsby project in a Github repository. There are ways of adding remote origins and things, but that’s for a different tutorial.

Login to your Github account. Create one if you don’t have one.

Create a new repository – you can create a private repository if you prefer.

Then clone your new repository to your development machine. In my case I use Ubuntu 20.04 on WSL2 to do my development. You’ll probably only have a README.md file and maybe a .gitignore if you started from scratch. If you already have a Gatsby project I think you can upload the non-versioned files directly to Github first, and then clone it to turn it into a version controlled project. Or you can use Github Desktop to convert it for you.

Then, create your Gatsby masterpiece. This isn’t a Gatsby tutorial either – there’s plenty of those around and even plenty that show you how to connect it to WordPress if you want. That’s what I did for this site in fact.

Make sure your Gatsby site builds locally before even attempting anything beyond this point. You will get very, very frustrated if your Gatsby project doesn’t even build locally and you try to start using Github Actions.

Create The Workflow Definition

You can do this on the Github website or you can do it within your Project locally. I created mine on Github initially but then realised it is just another source code file for your project. So I’d probably recommend just doing it locally and then pushing it up when you think it’ll work. It won’t work then, because something will be borked I’m sure. But it’s worth a try 🙂

The file to control the Github Actions workflow to deploy your main branch when you push the project needs to be stored in the following way.

.github/workflows/main.yml

So in your project, create the directory from your top level folder as .github/workflows – and then create a file called main.yml in that directory.

The main.yml is the main controller file for your Github Action. It should look something like this;

# This is a basic workflow to help you get started with Actions - you can change the name to be more reflective of your Action

name: CI

# Controls when the workflow will run
on:
  # Triggers the workflow on push request events but only for the main branch
  push:
    branches: [ main ]
    
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:
  

  # Allows A Webhook to be called which will run the Action. Use this for example from WordPress with the GatsbyJS plugin.
  repository_dispatch:
    types: [ publish_blog ]
    
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - name: Check out latest code
        uses: actions/checkout@v2

      # Grab the required version of NodeJS
      - name: Set Node.js
        uses: actions/[email protected]
        with:
          node-version: 14.x

     # Install all the Node dependencies for Gatsby, using your package.json file.
      - name: Install Dependencies
        run: npm i --save

     # Speaks for itself no?
      - name: Build Gatsby Site
        run: npm run build

     # This was the hardest part for me to figure out - see article for more details
      - name: Install Deployment SSH Key
        uses: shimataro/ssh-key-action@v2
        with:
          key: ${{secrets.DEPLOY_KEY}}
          known_hosts: 'Just a placeholder'

     # Finally, deploy it to your very own server
      - name: Deploy To Live
        run: rsync -avzh --delete -e 'ssh -o StrictHostKeyChecking=no' public/* --rsync-path='mkdir -p ${{secrets.target_dir}} && rsync' ${{secrets.ssh_user}}@${{secrets.ssh_host}}:${{secrets.target_dir}}

Github Action Workflow Steps Explained

Hopefully the embedded comments can give you a good idea of what the first few steps are all about. Basically this file is telling Github that when your code is pushed to the repository from your local development environment, it should be good enough to use to deploy. You’ll note I have no tests in here to ensure what you’ve done is right. I would advise building locally first using gatsby build and gatsby serve to test before pushing otherwise you’ll waste lots of time waiting for the Github build to either fail, or not produce the right results potentially.

The last two steps are where the magic really happens though. These two are the bits where I tripped up and spent literally hours (and multiple hours of build time on Github) to get right.

Install Deployment SSH Key

My initial attempts to get this working were guided by the very excellent guide by Kevin available at https://nehalist.io/building-and-deploying-gatsby-sites-with-github-actions/ and it’s fair to say that without Kevin’s article I would not have succeeded at all. But Kevin’s article uses a bit of an un-necessary step (perhaps it was necessary when he wrote it) in that he uses docker to do the deployment.

Docker isn’t needed. But rsync via SSH is still needed and I could not get Kevin’s method of setting up the SSH key to work. I had to go searching for an alternative plan. I found it at https://zellwk.com/blog/github-actions-deploy/ and so by combination of both previous authors I present the solution here.

So, the first step is going to be to produce an SSH key that can be used to authorise the Github workflow to access your server without needing interaction. For this reason the SSH key we produce is going to have no password associated with it. It should also be the only place that uses this key. Do not use your normal SSH key – create a new one. One that can be quickly revoked if somehow someone manages to get hold of it.

ssh-keygen -t rsa -b 4096 -C "[email protected]"

In my case I changed the e-mail to be “[email protected]” – don’t try to email that address, it doesn’t exist. But it helps me to see where in the next step my key is, in case I need to remove it quickly. The ssh-keygen command will ask you where to store the file. You should use a different name to the default. I chose github-deploy as my filename. This then created the Github deploy private key and github-deploy.pub public key.

Install Public SSH Key Onto Live Server

On the live server, ie, the machine you want to deploy your Gatsby generated site to, you’ll need to login and add the public key to the authorized_keys file for the SSH user who you will use to do the deployment. This might be the www-data user, or it might be the www user, or (and it should be) another user entirely who only has access to their own site. Nevertheless, copy the contents of the github-deploy.pub file into the ~/.ssh/authorized_keys file of the relevant user on the live deployment server.

Install Private SSH Key Onto Github

EEEEEK! You want me to do what now? Share my private SSH key with my repository?!?

No. Do not put your SSH keys in your repository at all, ever. Github provides a per repository ‘locker’ if you like, where you can store secret stuff such as usernames, passwords, API keys and even SSH private keys. They’re part of the repository called ‘Secrets’ and all of the secrets stored here are encrypted by Github and can’t be seen by anyone (including you) once they’re stored.

The action workflow can get access to these secrets though using template variables.

So, we want to create a repository secret called DEPLOY_KEY (actually Github says the keys are case insensitive). The name will be DEPLOY_KEY and the content for the secret will be the Private Certificate you created earlier. You should just copy and paste the text in. You may need to add an extra new line at the end depending on how you copied.

You’ll notice that the Install Deployment SSH Key step also requests a known_host: parameter. I’ve just put some default nonsense in there. You can go through the procedure of creating another secret containing the SSH known_host but I have disabled that check in the next step and I don’t worry too much about it. But it does potentially open you up to a ‘Man In The Middle’ attack in that someone could impersonate your deployment server and fake you into deploying on to their own server instead. If they had your public_key. And knew what username you’d set in the other secrets for the next step…

Deploy To Live

The final step then is the Deploy To Live step, which uses rsync to copy any changed files from the build process onto your live server. I use rsync because I don’t want Github to have to upload every single file every single time I run a build. It’s wasteful.

You’ll notice this step uses a few more secrets, such as target_dir, ssh_user and ssh_host. I use secrets for these because if my repository gets hacked somehow, I don’t really want to give people too much idea of how to begin attacking my live server. It’s a bit of security through obscurity perhaps but at least when couple up with other, more stringent security measures, it should suffice.

Test Your Workflow

I’d recommend adding the n flag to the rsync command in the example workflow above, to perform a dry run first. You may even want to make sure you have a backup of any directories that you might accidentally overwrite with this workflow. To dry run the command will look like;

run: rsync -avzhn (...)

If it all works you’re on to a winner. Take of the n and whenever you push any Gatsby code changes to the repository, Github will rebuild all your pages and deploy them to your own server. The process can take up to 10 minutes and varies depending on the complexity of your site. Some complex sites may well take longer that 10 minutes!

Hook WordPress Up To The Github Action Workflow Too

So, as you may have realised, I still use WordPress as the back end for the Most Useful website. But with the current setup of this Workflow you’d need to change the actual Gatsby code you’ve created in order to trigger a rebuild. But you won’t want to be doing that all the time, it’d be much better if it would re-generate your site whenever a post is created or updated.

Well, it just so happens, you can do that too. But for Github actions it needs a bit of work to get it happening. Here’s how to do it.

Install WP-Gatsby WordPress Plugin

The first part of the procedure is to install the WP-Gatsby WordPress plugin. At the time of writing you can download this from https://en-gb.wordpress.org/plugins/wp-gatsby/ and it’s an officially supported plugin made by the Gatsby Team. This plugin gives you the options of pinging a webhook when posts are created or modified and it will also ping a preview webhook if you want it to. I have done this and I’ll write an article about how to make that happen on your own server later.

Bear in mind, all of this is much easier on Gatsby Cloud, so if you’re a not for profit or a hobbyist, Gatsby Cloud is well worth a look. (You’ll still need the WP-Gatsby plugin even for Gatsby Cloud though).

Nevertheless, download the WP-Gatsby plugin into your WordPress system and Activate it. But you can’t put any webhooks in yet because unfortunately, we can’t talk directly to Github to tell it that posts have changed. This is because the Github API requires JSON data to be passed in an HTTPS POST request – and you can’t do that from WP-Gatsby

Create a Github Personal Access Token

To secure the API endpoints against someone maliciously rebuilding your site multiple times, or any other form of malicious intent via the Github API you need to create a Personal Access Token. Through the use of this token, you can tell Github what actions and permission this particular token is allowed to use.

In the case of rebuilding the Gatsby site when a post is updated, we’re going to call into the Github Repository Dispatch Event API. You may have noticed in the original main.yml configuration file for the Workflow, we told Github that a push would trigger the workflow and the repository_dispatch event would also trigger it. This is nice and easy because Github does the same procedure whether it’s a Gatsby code rebuild or a WordPress content rebuild.

So, we must first visit Your Account Settings (you can find them under the icon of yourself on the right hand side). Then we want Developer Settings and finally Personal Access Tokens. Before we go any further, get yourself a pen and paper, or a smartphone to take a photo. Or open Notepad on your computer 🙂 You’ll need to save the personal access token that is generated as it will never be available to you again after this.

Click Generate New Token and in the Note section give a couple of words about why you need this token. It’s only for your own reference, so something ‘Rebuild MySite’ or something will do. Then tick the boxes for the following;

Set Permissions for Personal Access Token
Set Permissions for Personal Access Token

Then click the green Generate Token button at the bottom of the page. The next page will show you the personal access token that Github has created and remember, you will not be able to recover this from Github. Copy it now. If you do lose it, you’ll just have to create a new one and update any code you have that uses it.

Create A ‘Proxy’ Script

So, if we look at the Github documentation for the repository_dispatch API Endpoint, we can see that we’ll need the Personal Access token in order to call the endpoint. We’ll also need to POST the request (rather than GET) and that means we can’t use the WP-Gatsby plugin directly. We need to create a helper script somewhere, which the WP-Gatsby plugin will call, and then the helper script talks to Github.

For my purposes, I’ve created a PHP script that looks like this and lives on a server that understands PHP. The WordPress server will work perfectly well if you can upload your own scripts to it. You’ll note that this is a very untidy script and I apologise. I may come back and attempt to clean this up. It’s also inherently insecure because, in my case, there’s nothing to check whether the caller is authorised to make the call. But on my server I have disallowed access to this script to anyone unless they’re calling it from the same server – so it’s a bit more secure.

<?php

$data = array(
    "event_type" => "publish_blog"
    );

$postData = json_encode($data);

$crl = curl_init('https://api.github.com/repos/[YOUR-GITHUB-USERNAME]/[YOUR-GITHUB-REPO]/dispatches');
curl_setopt($crl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($crl, CURLOPT_HEADER, true);
curl_setopt($crl, CURLOPT_POST, true);
curl_setopt($crl, CURLOPT_POSTFIELDS, $postData);
curl_setopt($crl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($crl, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'User-Agent: Most Useful cURL/1.0',
    'Authorization: token ghp_xxxxxxxxxxxxxxxxxxxxxxxxx'));

$result = curl_exec($crl);
// handle curl error
  if ($result === false) {
      // throw new Exception('Curl error: ' . curl_error($crl));
      print_r('Curl error: ' . curl_error($crl));
      $result_noti = 0; die();
  } else {
  print_r($postData);
 echo "Curl successful";
 print_r($result);
      $result_noti = 1; die();
  }
  // Close cURL session handle
  curl_close($crl);

?>

You’ll notice, on reading the script, that we create some JSON data for the POST payload. That data is pretty simple and contains the event_type: publish_blog trigger. You can change this event type if you like to something more pretty. But if you do, you must also change the main.yml that we created in the very beginning under the repository_dispatch setting.

You’ll also need to adjust [YOUR-GITHUB-USERNAME] and [YOUR-GITHUB-REPO] of course to match where your project is stored.

Upload Your Proxy Script

Once you’ve created the proxy script you’ll need to upload it, probably to the same server as your WordPress server. Secure it somehow (either by Apache/NGINX rules that only allow connects from the same machine as the WordPress installation, or by adding some shared key knowledge into the actual PHP script and ‘die()ing’ if that key isn’t presented in the GET request from WP-Gatsby) or both.

I uploaded mine and called it build.php for something different.

You can now test this script by running a wget request from the same server such as ‘wget https://my-wp-server.com/build.php’ for example and in another tab you can watch your Github Actions screen to see the progress of the Action. The PHP script will give you some feedback if Github rejects the request – but it’s not very pretty.

Tell WP-Gatsby To Prod Your Proxy

Once you’ve established that the proxy PHP script you uploaded above is working, you can tell WP-Gatsby to access the script whenever a post is updated. To do this, put the URL of your proxy script in the relevant box.

To get to the relevant box, you need to go to Settings -> GatsbyJS. The URL of your proxy script should be entered into the Builds Webhook option. If you’re not using Gatsby Cloud or your own Preview server, leave the Preview box unchecked. Incidentally, you can use Gatsby Cloud for preview and still deploy to your own server if you like. This is because Gatsby Cloud will also automatically rebuild on a Github push and will stay in sync with your Github repo if you’ve connected Gatsby Cloud to the Github repo.

Finale

If you’ve made it this far you’re some kind of hero. Or mad. Or both. Who says heroes aren’t mad anyway?

It’s a long post this one and I’ve really only touched the Gatsby Deployment to your own server via Github Actions and nothing else. Setting up a Gatsby Preview Server on your own server is a topic for another day, as is the whole idea of running Gatsby against WordPress. Incidentally, I highly recommend Gatsby with a Headless WordPress server these days. It’s what I use and it makes the end user experience considerably better than using WordPress itself for the end website visitor. WordPress has great admin interface and makes that side of things easy. But if you want speed and security, you need Gatsby generating a static site from your WordPress data.

Please do leave a comment if I’ve missed anything or anything is unclear. I think I’ve covered it all, and I’ve just set up two sites using this exact method, so hopefully it’s right 🙂

Thanks for reading! Good luck!