Deploying Jekyll Using Git and Bundler

As the custom dictates, the first post on my Jekyll-powered blog is about how to conveniently deploy Jekyll1 sites – with a twist: my workflow relies on Git to push things to the server and Bundler to manage dependencies.

After following the steps I’ll describe in detail below, you’ll be able to simply git commit any changes you’ve made to your site and then perform a git push to deploy those updates. That’s it!

(Also: Any Jekyll plugins you add to your Gemfile will be installed on the server automatically, and force-pushing won’t break anything.)

What you need

In order for this tutorial to make much sense, you need some basic experience with Jekyll2. You should also be reasonably comfortable working on remote servers using SSH.

Locally, you need a folder containing your Jekyll site and the associated Gemfile and Gemfile.lock files. Whether or not Jekyll is installed locally doesn’t matter for the deployment process, but since it allows you to test any changes locally, it’s recommended.

On the server where you want to deploy your site, Ruby, RubyGems and Bundler should already be installed. Conveniently, all of these are installed out of the box on Uberspace, which is where I host this blog.

Additionally, you’ll need reasonably up-to-date versions of Git both locally and on your server.

What you have to do

Okay. You’ve finished the initial setup work of getting a Jekyll site up and running – but so far only locally on your laptop. In order to get it up into the cloud3, you need a server. You’ve got that too? Great! Let’s get started.

Because you’ll be using Git to deploy your site, you need two Git repositories: one locally (in which you’ll perform your changes) and another one on the server (where you’ll push your changes, and which will manage the Jekyll build).

Unless you already have a local Git repository, it’s best to create one on the server first. On your server, run the following commands to create a bare repository.

$ mkdir <REPOSITORY NAME>.git
$ cd !$
$ git init --bare

To create the local repository, clone the one you’ve just created. Locally, run:

$ git clone ssh://<USER>@<SERVER.TLD>/<ABSOLUTE PATH TO REPOSITORY>
$ cd <REPOSITORY NAME>

Now you can move your Jekyll site to that folder and git add and git commit your changes. But before pushing them to your server by means of git push, let me tell you about Git hooks:

Every Git repository contains a hidden .git/ directory. This is where Git stores pretty much everything it needs to know about that repository: its configuration, the commit history, and among a bunch of other things, a hooks/ folder. This folder contains .samples for shell scripts that Git will run before or after specific actions are performed – pre-push will be executed right before pushing to a remote server, pre-commit will run before Git registers a commit, and so on. You get the idea: these scripts allow you to hook some custom actions into Git’s internal pipeline.

How can you use this feature? There’s a hook called post-receive, which Git will conveniently run for you whenever changes are pushed to the containing repository. So you basically just need to call jekyll build somewhere in the post-receive file, and Git will take care of kicking off the build process whenever you push changes to the server!

It’s not actually quite that simple, but that’s the basic process. To create the hook, run the following commands on your server:

$ cd <REPOSITORY NAME>.git/hooks
$ touch post-receive
$ chmod +x post-receive
$ nano post-receive

In the text editor that pops up, paste the following shell script4 and save after modifying the first three lines (which I’ll explain below) to match to your setup:

GIT_REPO="$HOME/<REPOSITORY NAME>.git"
TMP_GIT_CLONE="$HOME/tmp_<REPOSITORY NAME>"
PUBLIC_WWW="<WEBSITE FOLDER>"

unset GIT_DIR

if [ ! -d "$TMP_GIT_CLONE" ]; then
    git clone $GIT_REPO "$TMP_GIT_CLONE"
    cd "$TMP_GIT_CLONE"
else
    cd "$TMP_GIT_CLONE"

    # avoid breaking the build when force pushing
    git fetch --all
    git reset --hard origin/master

    git pull
fi

# make sure that everything's installed
bundle install --deployment

# kick off the build
bundle exec jekyll build -s "$TMP_GIT_CLONE" -d "$PUBLIC_WWW"

exit

The GIT_REPO variable should contain the path to the bare Git repository you’ve previously created on your server. The directory pointed to by TMP_GIT_CLONE will contain a temporary clone of the bare repository. This directory will be created automatically, so there’s no need to mkdir it before pushing your first commit! Finally, the PUBLIC_WWW variable should point to wherever Jekyll should write the finished site to.

The shell script is fairly self-explanatory, so I won’t confuse you by rehashing what it does.

Note that bundle install --deployment will create a folder $TMP_GIT_CLONE/vendor, where all required Gems will be installed5. Some files in this directory will invariably contain Liquid tags, so it’s best to tell Jekyll to ignore it. Adding the following line to your _config.yml file does the trick:

exclude: [vendor]

Now you can finally run git push. If everything goes well, you should see something along these lines:

Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 337 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Cloning into '/home/doersino/tmp_hejnoah.com' ...
remote: Done.
remote: Warning: the running version of Bundler (1.13.2) is older than the versi
on that created the lockfile (1.13.5). We suggest you upgrade to the latest vers
ion of Bundler by running `gem install bundler`.
remote: Fetching gem metadata from https://rubygems.org/..........
remote: Fetching version metadata from https://rubygems.org/..
remote: Fetching dependency metadata from https://rubygems.org/.
remote: Installing addressable 2.4.0
remote: Installing colorator 1.1.0
remote: Installing ffi 1.9.14 with native extensions
remote: Installing forwardable-extended 2.6.0
remote: Installing sass 3.4.22
remote: Installing rb-fsevent 0.9.7
remote: Installing kramdown 1.12.0
remote: Installing liquid 3.0.6
remote: Installing mercenary 0.3.6
remote: Installing rouge 1.11.1
remote: Installing safe_yaml 1.0.4
remote: Using bundler 1.13.2
remote: Installing rb-inotify 0.9.7
remote: Installing pathutil 0.14.0
remote: Installing jekyll-sass-converter 1.4.0
remote: Installing listen 3.0.8
remote: Installing jekyll-watch 1.5.0
remote: Installing jekyll 3.3.0
remote: Installing jekyll-feed 0.8.0
remote: Bundle complete! 3 Gemfile dependencies, 19 gems now installed.
remote: Bundled gems are installed into ./vendor/bundle.
remote: Configuration file: /home/doersino/tmp_hejnoah.com/_config.yml
remote:             Source: /home/doersino/tmp_hejnoah.com
remote:        Destination: /var/www/virtual/doersino/hejnoah.com
remote:  Incremental build: disabled. Enable with --incremental
remote:       Generating...
remote:                     done in 0.567 seconds.
remote:  Auto-regeneration: disabled. Use --watch to enable.
To ssh://draco.uberspace.de/home/doersino/hejnoah.com.git/
   9dda0ee..c2285dd  master -> master

Addendum: Open-sourceing your site

If you want to push your site to a GitHub repository6 in addition to pushing to your deployment server, the instructions up until the Git hooks part are slightly different. I’ll just show you what I did – you’ll need to modify the URIs to match your setup.

On your server, git clone --bare the GitHub repository:

$ git clone --bare https://github.com/doersino/hejnoah.com.git

Locally, simply create a normal git clone of the GitHub repository:

$ git clone https://github.com/doersino/hejnoah.com.git

Now you need to tell Git to push your local clone to both the original GitHub repository and the bare repository you’ve just created. You can achieve this by editing the .git/config file of the local clone. In the [remote "origin"] section, add your equivalent of the following two lines:

pushurl = https://github.com/doersino/hejnoah.com.git
pushurl = ssh://doersino@draco.uberspace.de/home/doersino/hejnoah.com.git/

Now whenever you git push, Git will first push to GitHub and then to your deployment server, where the post-receive hook will fire, which will kick off the Jekyll build. A second or three later, the changes you’ve made will be live on GitHub as well as on your site!

  1. At the time of writing, Jekyll 3.3 is the newest release. Future updates might break things.

  2. My first time working with Jekyll was a few days ago, while making the theme for this blog. This post is solely based on that experience – so if you’ve got your own, brand new Jekyll site, you’ll probably be able to follow along easily.

  3. …you strap it on top of a rocket and get Elon Musk to press the red button.

  4. Based on a post by Eric Oestrich and the Jekyll deployment guide.

  5. If you’re a Haskell geek: This is roughly analogous to the .cabal-sandbox directory.

  6. Or, more generally, to a second Git repository – whether it lives on GitHub, GitLab, Bitbucket or anywhere else doesn’t really matter.