Jekyll is a fantastic tool to develop fast, reliable websites. It churns out pure HTML (possibly enriched with 3rd party APIs like Disqus), so it’s fairly hackproof – unlike, say, WordPress. Writing content for Jekyll is also a snap: just create some markdown, build and check your site, and you’re ready to upload. Still, having to upload the whole site through FTP every time takes a lot of work. Wouldn’t it be nice if that were automated?

Enter GitLabs Continuous Integration / Continuous Deployment (CI/CD).

What is CI/CD?

It’s always a good idea to put your code in a git repository, and store it with a service like GitHub or GitLab. GitLab has the advantage that it allows you to create private repositories, free of charge. But there’s more: it also comes with a set of tools that can build and deploy your code (that is, your website) every time you commit a change to the git repository.

GitLabs offers Continuous Integration / Continuous Deployment. In a nutshell, whenever you commit some code, a process on GitLab launches that builds that code, tests it, and even deploys it - according to your specifications. Builds and tests can fail, and GitLab will let you know with an email, if you like.

Setting up CI/CD

In order to get CI/CD to work for you, you’ll need to add a file to your Jekyll codebase: gitlab-ci.yml. It’s a YAML file that tells GitLab which steps to take whenever you commit new code. Without, GitLab’s CI/CD process does nothing.

In gitlab-ci.yml (which must live at the root of your repository), start with this:

image: ruby:2.6

The first thing GitLab needs to know is what environment to build your code in. For each build, GitLab is going to launch a special virtual machine, and you get to specify what software should be present on it. There are virtual machines for NodeJS, for PHP, for Ruby, or a combination of these, and much else. For Jekyll, all we’re going to need is Ruby.

Adding a stage

CI/CD processing on GitLab is done in stages. You can have a build stage, where GitLab compiles your code (or builds your Jekyll, in our case), and a deploy stage, where you have GitLab copy the build results to your webserver. Let’s start by creating a stage that builds our Jekyll source:

pages:
  stage: build
  script:
    - bundle install --path vendor
    - bundle exec jekyll build -d public
  only:
  - master

Here we have a stage build with a single job pages. A stage can have many jobs, but we’ll only need one. Inside the pages block, we have a script section that tells GitLab with steps to take. First, we have it install the gem bundle that comes with our Jekyll project. (You should have a gemfile in your repository already, if you were able to build the Jekyll source on your local machine.) GitLab will download all gems and install them on its virtual machine. Next, we tell Jekyll to build our site from source, putting the result in a folder named public. We also tell GitLab to process only the master branch, just in case you work with multiple git branches.

If you were to commit this file right now to your GitLab repository, GitLab will immediately see it and start executing this process. You can see it work when you log into GitLab and visit the CI/CD tab, then Pipelines. You should find a pipeline that is marked as “running”, and you can even click it to see the exact output as it appears.

Generating an artifact

Currently, GitLab builds your Jekyll site from source and places the result in the /public directory. That’s great, but as soon as the process is done, the virtual machine shuts down and the results are gone. To get a hold of them, we need to have GitLab create an artifact. Change the gitlab-ci.yml to this:

pages:
  stage: build
  script:
    - bundle install --path vendor
    - bundle exec jekyll build -d public
  artifacts:
    paths:
    - public
  only:
  - master

The artifacts section tells GitLab to save the contents of the /public directory that we produced. When you run the pipeline again (by committing this change), you will see that - when processing is complete - the pipeline offers a file to download. (There will be a little cloud icon at the right end of the pipeline entry in GitLab). This file contains the zipped content of the /public directory.

You could now go ahead and upload the contents of this zipfile to your webserver. But there is much more fun to be had.

Caching the bundles

As an aside, installing Jekyll’s gemfiles takes a bit of time, and it has to be done each time GitLab starts a virtual machine to build your code. You can speed this up a bit by adding this to your gitlab-ci.yml file:

cache:
  paths:
  - vendor/

This will create a cache of the installation procedure and use it the next time it’s run.

Adding a deploy stage

After building the Jekyll site from code, what we’d really like is for GitLab to copy the resulting files to our webserver, through FTP. To do so, we create a new stage named deploy. It’s run when the pages stage is complete. In your gitlab-ci.yml, add:

ftp:
  stage: deploy
  script:
    - apt-get update -qy
    - apt-get install -y lftp  
    - lftp -e "set ftp:ssl-allow no; open ftp.independent-software.com; user $FTP_USERNAME $FTP_PASSWORD; mirror -X .* -X .*/ --reverse --verbose public/ public_html/; bye"

What’s going on here? There is, by default, no ftp software present on GitLab’s virtual machine. You could either find a Docker image that has ftp installed and use that, or install it yourself. We’re using a Ruby image that doesn’t have ftp, but we can install it by running:

apt-get update -qy
apt-get install -y lftp  

This will install lftp, an ftp package that can take a command line argument to connect to an FTP server and execute operations, which is what happens in the next line:

lftp -e "set ftp:ssl-allow no; open ftp.myserver.com; user $FTP_USERNAME $FTP_PASSWORD; mirror -X .* -X .*/ --reverse --verbose public/ public_html/; bye"

Here, we launch lftp with the -e flag, which takes a command string. In the string, we open a connection to our server using open ftp.myserver.com;, then login with user $FTP_USERNAME $FTP_PASSWORD;, then mirror our public directory to the server’s public_html directory with mirror -X .* -X .*/ --reverse --verbose public/ public_html/. Change paths as needed. Finally, we close the connection with bye.

There are a couple of points to note here. First, I’ve added set ftp:ssl-allow no;. This allows lftp to make connections to server with do not support SSH connections. An SSH connection is always better, so remove this if your server has support; otherwise bad men may sniff your username and password from the line.

Second, there are two variables, FTP_USERNAME and FTP_PASSWORD. You could have just put your actual password in the lftp command string, but that would make it visible to anyone with access to your git repository. Storing it as a variable solves that problem. Variables are set in GitLab under Settings -> CI/CD -> Variables. Set your password variable’s state to “protected” for good measure.

When you run this pipeline (by committing the changes), GitLab will first build your Jekyll site, and then - provided there were no build errors - FTP the result to your web server, straight into /public_html, ready to be served!

Result

Here is the full script, if you’re in a hurry:

image: ruby:2.6

cache:
  paths:
  - vendor/
  
pages:
  stage: build
  script:
    - bundle install --path vendor
    - bundle exec jekyll build -d public
  artifacts:
    paths:
    - public
  only:
  - master

ftp:
  stage: deploy
  script:
    - apt-get update -qy
    - apt-get install -y lftp  
    - lftp -e "set ftp:ssl-allow no; open ftp.myserver.com; user $FTP_USERNAME $FTP_PASSWORD; mirror -X .* -X .*/ --reverse --verbose public/ public_html/; bye"

This script is, in fact, precisely what I use to update this website.