[recipe, sysadmin]: How to deploy a hugo-powered static website to a webserver with GIT
This website is powered by Hugo, an open-source static website generator written in Go. I can write pages and posts in Markdown and then generate some static .html pages to upload to a webserver.
For this project, as I usually do, I push every change to a self-hosted GIT server with Gogs (another open-source, golang written GIT server), so why not use webhooks to automate the deployment?
Gogs, as GitHub, GitLab and other well-known services provides webhooks to notify other services of changes in a repository: they are basically HTTP callbacks triggered by some user-defined events such as a push or a pull request. Each time you, for instance, push to a repo, Gogs will send a HTTP POST payload to the webhook’s configured URL: the payload will contain the relevant event information.
To protect from unauthorized requests you could tell Gogs to send, in the header of the POST request, a X-Gogs-Signature
that contains the HMAC hex digest of the payload generated using the sha256
hash function and a secret as the HMAC key.
My event-chain is the following: I push the new content to the repo, Gogs trigger a webook that send a HTTP POST payload to a .php page in the webserver. The php-page pulls the repo to the server, generates the static website with hugo
and copies the newly generated public
directory to the document root of the webserver.
So, let’s see the practical part:
Add in the Gogs repository configuration an ssh Deploy Key, to pull the repo in the webserver. The deploy key will have read-only access. You can generate the ssh key with
ssh-keygen
but keep it password-less to make the pull completely automated.Clone the repo in the document root with
git clone user@git.server:repo /var/www/
Insert in your
.htaccess
file the secret like that:SetEnv GOGS_DEPLOY_SECRET y0uRs3cRetC0d3
Create a
deploy.php
page that will pull the repo, generate the website and deploy it when called. You can use the following script:<?php /** * Automated deploy from Gogs * * Template from ServerPilot (https://serverpilot.io/community/articles/how-to-automatically-deploy-a-git-repo-from-bitbucket.html) * Hash validation from liogate (https://github.com/gogs/gogs/issues/4233#issue-211797295) */ // Variables $secret = getenv('GOGS_DEPLOY_SECRET'); $repo_dir = '/path/to/repo/'; $web_root_dir = '/var/www/'; $rendered_dir = '/public'; $hugo_path = '/usr/local/bin/hugo'; // Validate hook secret if ($secret !== NULL) { // Get signature $gogs_signature = $_SERVER['HTTP_X_GOGS_SIGNATURE']; // Make sure signature is provided if (!isset($gogs_signature)) { file_put_contents('deploy.log', date('m/d/Y h:i:s a') . ' Error: HTTP header "X-Gogs-Signature" is missing.' . "\n", FILE_APPEND); die('HTTP header "X-Gogs-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.'); } // Get payload $payload = file_get_contents('php://input'); // Calculate hash based on payload and the secret $payload_hash = hash_hmac('sha256', $payload, $secret, false); // Check if hashes are equivalent if (!hash_equals($gogs_signature, $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 Gogs hook payload $data = json_decode($_POST['payload']); $commit_message; if (empty($data->commits)){ // When merging and pushing to Gogs, 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 pull, run Hugo, and copy files to public directory exec('cd ' . $repo_dir . ' && git pull'); 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); }
In the Gogs repository webpage, activate a webhook, specifying the
deploy.php
URL, the content type (application/json
) and a secret. Then select the event(s) that will trigger the webhook.You’re done! Write a new post, push it and see the magic ;)