Automating the deployment of your static site on your own server | Chris Ferndinandi

Need to self-host your front end away from the "modern" services like Netlify or Vercel? As both continue to get a little sketchier with time, it's definitely something I'm having to consider. Deploying a static site to an old-school host (like my favourite: Krystal) is easy enough, but you lose that wonderful "rebuild on Git push" that we've all become accustomed to. Or do you?

Whenever I push an update in my websites master branch in GitHub, it notifies my server through something called a webhook. My server pulls the latest version of my site from GitHub, runs a Hugo build, and moves the built files into my live site directory.

Chris has written up how to achieve similar functionality on Digital Ocean. I'm not sure how well this would translate to other services, but the toolchain seems generic enough. You need:

  • A GitHub account;
  • Some form of PHP hosting with control over file and folder placement and SSH/CLI access;

And that seems to be about it.

The steps look like this:

  1. Set up your repo on GitHub;
  2. Install whatever build tool you want to use on your server (Chris uses Hugo, but any front-end framework should work the same way);
  3. Create an SSH key for the server;
  4. Clone your GitHub repository to the server via SSH: git clone<YOUR_USERNAME>/<YOUR_RESPOSITORY>.git
  5. Create an automation script within a PHP file; Chris uses deploy.php and the code below;
  6. Add a deploy secret as an environment variable (likely in the htaccess file);
  7. On GitHub, set up a webhook with the "payload URL" as your domain plus the name of the PHP script, e.g:;
  8. Test it out.

Here's the code (note Chris' version may be more up to date, just saving here in case that post disappears):


     * Automated deploy from GitHub
     * Template from ServerPilot (
     * Hash validation from Craig Blanchette (

    // Variables
    $secret = getenv('GH_DEPLOY_SECRET');
    $repo_dir = '/srv/users/serverpilot/apps/<YOUR_APP>/build';
    $web_root_dir = '/srv/users/serverpilot/apps/<YOUR_APP>/public';
    $rendered_dir = '/public';
    $hugo_path = '/usr/local/bin/hugo';

    // Validate hook secret
    if ($secret !== NULL) {

        // Get signature
        $hub_signature = $_SERVER['HTTP_X_HUB_SIGNATURE'];

        // Make sure signature is provided
        if (!isset($hub_signature)) {
            file_put_contents('deploy.log', date('m/d/Y h:i:s a') . ' Error: HTTP header "X-Hub-Signature" is missing.' . "\n", FILE_APPEND);
            die('HTTP header "X-Hub-Signature" is missing.');
        } elseif (!extension_loaded('hash')) {
            file_put_contents('deploy.log', date('m/d/Y h:i:s a') . ' Error: Missing "hash" extension to check the secret code validity.' . "\n", FILE_APPEND);
            die('Missing "hash" extension to check the secret code validity.');

        // Split signature into algorithm and hash
        list($algo, $hash) = explode('=', $hub_signature, 2);

        // Get payload
        $payload = file_get_contents('php://input');

        // Calculate hash based on payload and the secret
        $payload_hash = hash_hmac($algo, $payload, $secret);

        // Check if hashes are equivalent
        if (!hash_equals($hash, $payload_hash)) {
            // Kill the script or do something else here.
            file_put_contents('deploy.log', date('m/d/Y h:i:s a') . ' Error: Bad Secret' . "\n", FILE_APPEND);
            die('Bad secret');


    // Parse data from GitHub hook payload
    $data = json_decode($_POST['payload']);

    if (empty($data->commits)){
        // When merging and pushing to GitHub, the commits array will be empty.
        // In this case there is no way to know what branch was pushed to, so we will do an update.
        $commit_message .= 'true';
    } else {
        foreach ($data->commits as $commit) {
            $commit_message .= $commit->message;

    if (!empty($commit_message)) {

        // Do a git checkout, run Hugo, and copy files to public directory
        exec('cd ' . $repo_dir . ' && git fetch --all && git reset --hard origin/master');
        exec('cd ' . $repo_dir . ' && ' . $hugo_path);
        exec('cd ' . $repo_dir . ' && cp -r ' . $repo_dir . $rendered_dir . '/. ' . $web_root_dir);

        // Log the deployment
        file_put_contents('deploy.log', date('m/d/Y h:i:s a') . " Deployed branch: " .  $branch . " Commit: " . $commit_message . "\n", FILE_APPEND);


Explore Other Notes


Jamie's bookshelf

I'm a sucker for a personal collection displayed on the web, and I really love the simplicity of Jamie's design for his digital bookshelf. An easy rating system; simple (and fast) filters; and a very […]


EAA: what you need to know

An excellent overview of the European Accessibility Act, how it overlaps with existing regulations, and the impact it might have. Doesn't get too technical, but does do a good job of explaining the […]
  • Need to self-host your front end away from the "modern"&nbsp;services like Netlify or Vercel? As both continue to get a little <em>sketchier</em> with time, it's definitely something I'm having to [&#8230;]
  • Murray Adcock.
Journal permalink

Made By Me, But Made Possible By:


Build: Gatsby

Deployment: GitHub

Hosting: Netlify

Connect With Me:

Twitter Twitter

Instagram Instragram

500px 500px

GitHub GitHub

Keep Up To Date:

All Posts RSS feed.

Articles RSS feed.

Journal RSS feed.

Notes RSS feed.