Deploying a Static Blog with Continuous Integration

In recent years, static sites have seen a resurgence. In this post we'll explore how to use Hugo, a static site generator, in conjunction with a remote web server and a continuous integration provider. Although the site generation software here is Hugo it could easily be another provider such as Jekyll. The web server will be hosted on a Digital Ocean droplet running Nginx, and CircleCI will be used for for the continuous integration (often abbreviated to simply "CI"). Although it is difficult to speak in specifics due to the multitude of alternatives out there, it should be (hopefully) fairly straightforward to deduce a similar process for other providers.

Firstly, for those of you unfamiliar what Continuous Integration is, here is a definition:

"Continuous Integration is a DevOps software development practice where developers regularly merge their code changes into a central repository, after which automated builds and tests are run" (Amazon Web Services, 2017)

Generally CI is leveraged when there are many developers checking in code to a distributed version control system to ensure the integrity of software before its release to some environment. However we can leverage another one of its key tenets, which is that the building, deployment and testing is being done by a remote service, saving us time and energy. This in turn allows you to focus on writing the blog posts and not how or where the content needs to be deployed.

Some of you may be deploying your static Hugo site (or other static blog) via a bash script or some sort of manual process. This guide will explain how to deploy your blog every single time you commit that code to GitHub, or other cloud based git provider.

Why Bother? #

There are a few benefits to setting up CI with your static blog:

Setting Up Circle CI and SSH Keys #

You will need to register for an account with CircleCI and associate it with your GitHub account. From here you can begin to build GitHub projects from CircleCI. Check out this guide from CircleCI for more specifics regarding that process.

Before we dig into the details, it is important to explain that in order to get the Circle CI talking to our remote server (this case a Digital Ocean droplet), we must setup SSH keys. SSH allows keys allow us to be authenticated by the server whilst avoiding the use of passwords via Public-key cryptography.

It is a little outside the scope of this guide to delve into setting up the keys, however a great guide to generating SSH keys is available from GitHub here, and an explanation of how to use them in conjunction with Circle CI can be found in their docs here. Once you have generated the keys and registered them with CircleCI you can move onto the next section.

Environment Variables #

It is desirable to avoid storing any secrets or sensitive information inside the blogs source code itself. One way to do this is to use environment variables on the CI server. CircleCI provides a UI for setting environment in the project settings, on the left hand panel.

Env Vars

For example in this case, the user login password is set as DIGITALOCEAN and the IP of the server is DIGITALOCEAN_IP. This also makes them reusable should you need to use them more than once in your configuration scripts.

The Configuration File #

Once our target server and CircleCI are setup with the right keys we can begin to look into how to deploy from CircleCI. With CircleCI we provide a configuration file in the form of 'circle.yml'. The config is written in YAML which is a minimalist markup language, often used for configs. Here is what my specific circle.yml file looks like:

dependencies:
pre:
- wget https://github.com/gohugoio/hugo/releases/download/v0.23/hugo_0.23_Linux-64bit.deb
- sudo dpkg -i hugo*.deb
- sudo apt-get install sshpass rsync
- cd ./themes/impurehugo/ && npm install

deployment:
prod:
branch: master
commands:
- cd ./themes/impurehugo/ && npm run build
- hugo -v
- sudo sshpass -p "$DIGITALOCEAN" rsync -avz ./public $DIGITALOCEAN_IP:/var/www/html/

test:
override:
- "true"

You can see in the dependencies section we install hugo at the current latest release (v0.23), and we install the package. We also install sshpass as we will need it to login on the target deployment server.

In my case I do a little extra work with npm and gulp to do some preprocessing (JavaScript/CSS minification, image compression) so we change to the target theme folder and do an npm install there to ge the node dependencies and install gulp. This step can be skipped if you aren't interested in these preprocessing steps.

With deployment, we pull from master and then we do the frontend pre-processing previously mentioned with gulp compress. After this we run Hugo with the verbose flag (useful for debugging purposes if necessary). Next because Hugo only produces static assets, we can simply move them over to our target server. Here we use rysnc to copy the files to the remote server. rsync has the benefit over scp that it only transfers files that have changed since the last upload. However, you could use something like scp if you are so inclined.

Lastly we override the tests section to simply pass if as our only real major concern is if there are errors in the actual build and deployment steps.

Conclusion #

This should hopefully give an overview of how to setup continuous integration with Hugo, and hopefully enough inspiration to adapt it to work with other static site providers if necessary. I really welcome any improvements, requests for clarity of other feedback you might have for me. Feel free to reach out to me on Twitter, or drop me an email.

Published