Hello and welcome to the first post on my blog. In this post I will show you how you can deploy your Jekyll site to a Linux server in an automated way by using Git hooks. Hooks provide a simple way to automatically execute scripts when something changes in a repository. In our case, we want to set up a hook on a remote repository which automatically builds a Jekyll site whenever whe push a change into this repository. Deploying a website in such an automatic way is useful especially if multiple developers work on the same website and you want your changes to be directly reflected by your web server.

User Setup

First, we create a new user on the server called blog. This user hosts the remote repository and runs the Jekyll builds. The home directory is set to /var/www/blog. The shell is set to /usr/bin/git-shell. This gives a restricted non-interactive shell which is only usable for Git commands.

sudo useradd -d /var/www/blog -m -s /usr/bin/git-shell blog

Next, we declare the public key of our local machine so that we can connect to the repository via SSH. For this, we open a shell with the new blog user. We create a new directory named .ssh and set the permissions such that only the blog user is allowed to read, write and execute this directory. This is good practise as you may also store private keys in this directory. Next, we pipe the public key of our local machine into the authorized_keys file. Replace the given public key string with your real public key.

sudo -u blog bash
mkdir .ssh
chmod 700 .ssh
echo "your public key" > .ssh/authorized_keys

Next, we set up the Ruby gem home directory of the blog user. This allows us to locally install bundler and further gems used by the Jekyll site. For this, we append two lines to the file .profile. The first line sets the Ruby gem home variable which specifies where the gems will be stored. For this, a ruby command is executed which prints a default user gem directory structure. This results in a string like /var/www/blog/.gem/ruby/2.3.0/bin. The second line adjusts the PATH variable such that it refers to the bin directory of the new Ruby gem home. This allows us to directly use the gems in the later hook script.

export GEM_HOME=$(ruby -e 'print Gem.user_dir')
export PATH="$(ruby -e 'print Gem.user_dir')/bin:$PATH"

Now we install the bundler gem which is needed to download Jekyll and additional dependencies of the Jekyll website. Before that, we have to source .profile so the changes to the PATH and Ruby gem home become active.

. .profile
gem install bundler

Remote Repository Setup

Now comes the interesting part, namely the setup of the remote repository and the hook. First, we create a bare Git repository in /var/www/blog/jekyll-blog.git.

mkdir jekyll-blog.git
cd jekyll-blog.git
git init --bare

Next, we set up a so-called post-receive hook by creating the file hooks/post-receive in our new Git repository and making it executable. This file will contain the script which will be executed after the server received the changes we pushed.

touch hooks/post-receive
chmod +x hooks/post-receive

Now we insert the following shell script into this file. The script basically iterates over the Git refs it receives and searches for the master ref. Therefore, the Jekyll site is only built when we pushed changes to the master branch of the repository. We source .profile which sets the Ruby gem home directory and PATH variable. This is mandatory because the hook is executed within a cleaned environment, i.e. only Git specific variables are set. Next, the working directory is created if not existing and the current Jekyll files of the repository are checked out there. Then bundler is executed which installs Ruby gem dependencies into the local gem home. Finally, Jekyll is executed which builds the site in the working directory and stores the output in a subdirectory named _site.

#!/bin/sh

JEKYLL_WORKDIR="/var/www/blog/jekyll-blog"

while read oldrev newrev ref
do
  if [ $ref = refs/heads/master ];
  then
    # source .profile for the Ruby gem home and adjusted PATH variable
    . /var/www/blog/.profile

    echo "received master ref, starting deployment ..."
    mkdir -p $JEKYLL_WORKDIR

    echo "checking out working copy ..."
    git --work-tree="$JEKYLL_WORKDIR" checkout -f

    echo "running bundle install ..."
    bundle install --gemfile="$JEKYLL_WORKDIR/Gemfile"

    echo "running jekyll build ..."
    jekyll build -s $JEKYLL_WORKDIR -d $JEKYLL_WORKDIR/_site

    echo "finished"
  else
    echo "received other ref, doing nothing as only master ref is built"
  fi
done

Pushing Changes to Production

We can now test if the hook is executed by pushing changes to the remote repository. For this, we set up a local repository and a new Jekyll site. We define a new remote called production and create an initial commit. The new remote is added by specifying the url of the remote repository. Replace the given host name with the real host name of your remote server.

mkdir jekyll-blog
cd jekyll-blog
git init
git remote add production blog@your.host.name:jekyll-blog.git
jekyll new .
git add .
git commit -m "Initial commit"

Finally, we push the changes we made in our local repository to production.

> git push production master
Counting Objects: 11, Finished.
Delta compression using up to 4 threads.
Compressing Objects: 100% (10/10), Finished.
Writing Objects: 100% (11/11), 3.72 KiB | 0 bytes/s, Finished.
Total 11 (delta 0), reused 0 (delta 0)
remote: received master ref, starting deployment ...
remote: checking out working copy ...
remote: running bundle install ...
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: Using public_suffix 2.0.5
remote: Using colorator 1.1.0
remote: Using sass 3.4.24
remote: Using rb-fsevent 0.9.8
remote: Using ffi 1.9.18
remote: Using kramdown 1.13.2
remote: Using liquid 4.0.0
remote: Using mercenary 0.3.6
remote: Using forwardable-extended 2.6.0
remote: Using rouge 1.11.1
remote: Using safe_yaml 1.0.4
remote: Using bundler 1.15.1
remote: Using addressable 2.5.1
remote: Using jekyll-sass-converter 1.5.0
remote: Using rb-inotify 0.9.10
remote: Using pathutil 0.14.0
remote: Using listen 3.0.8
remote: Using jekyll-watch 1.5.0
remote: Using jekyll 3.5.0
remote: Fetching minima 2.1.1
remote: Installing minima 2.1.1
remote: Using jekyll-feed 0.9.2
remote: Bundle complete! 4 Gemfile dependencies, 21 gems now installed.
remote: Use `bundle info [gemname]` to see where a bundled gem is installed.
remote: running jekyll build ...
remote: Configuration file: /var/www/blog/jekyll-blog/_config.yml
remote:             Source: /var/www/blog/jekyll-blog
remote:        Destination: /var/www/blog/jekyll-blog/_site
remote:  Incremental build: disabled. Enable with --incremental
remote:       Generating... 
remote:                     done in 0.879 seconds.
remote:  Auto-regeneration: disabled. Use --watch to enable.
remote: finished
To your.host.name:jekyll-blog.git
 * [new branch]      master -> master

While the changes are pushed, we get the output of the post-receive hook execution. We see how bundler installs the required Ruby gems for the Jekyll site and how Jekyll builds the website. The final output is stored in /var/www/blog/jekyll-blog/_site. You can now use a web server on your remote host to serve requested content out of this directory. Whenever you push changes to production, your web server will directly reflect these changes.

Conclusion

In this post I showed you how to automatically deploy a Jekyll website to a remote server using Git hooks. For this, we created a new user on the remote machine and set up public-key authentication. We adjusted the Ruby gem home directory such that gems could be installed and used locally. Next, we installed bundler in order to get Jekyll and the required Ruby gems for the Jekyll website. Then we set up the Git repository and created the post-receive hook which built the Jekyll website once we pushed changes to the master branch. Finally, we tested the hook by setting up a local Git repository and a new Jekyll website. We set up a remote called production and pushed the initial commit. We could see how bundler installed the required Ruby gems and Jekyll built the website.

How do you deploy your Jekyll website? Do you simply copy your locally generated files or do you use a more advanced technique than described in this post? Please let me know in the comments section below. Also, don’t hesitate to write me if you have any feedback or question regarding this topic. I hope you enjoyed my first blog entry. Stay tuned for the upcoming posts.