When developing software, and especially Web applications, I’ve found that thinking about the best deployment method early on is usually a good thing. Solving the deployment situation answers the question: What’s the best way to make the latest version of my software available to users?

The answer very much depends on the software being developed, and the tools used to do so. I’ve recently played with a tool called Jekyll, so I’ll talk about that.

Jekyll

Jekyll is a simple but powerful tool for creating Web sites. It’s a little different from Web application frameworks like Django, or other similar frameworks that are available.1 A traditional Web application is typically backed by a database and supports dynamic content generation. When a request comes in from a Web browser the application first needs to determine what the request is for. Then it calls the code in the application that is responsible for handling the request. The application might fetch some data from a database, and then use a template engine to iterate over the data to generate the final page content. Finally, the result is returned and sent over the wire to the requesting browser. The backend server is not just returning static files, but runs application code that handles requests and returns results based on the request and the current state of the database.

Jekyll works very much the same way, but with the difference that all the resources available to be requested are pre-generated. For this reason, Jekyll and similar frameworks are often referred to as static Web site generators. Keep in mind that Jekyll is very different from hand-coding your own HTML pages. Jekyll provides an advanced template system, enables data-driven page generation, has built-in support for Markdown and Sass, and can incrementally re-compile pages when their content changes.

The whole point of a static Web site generator is that it pre-generates the content to be served, i.e. the Web pages. This greatly simplifies content deployment. The entire Web site can be deployed as simple resources on a Web server (e.g. nginx), just like images or other static content. There is no need to run a separate application on the backend server to interpret requests and determine the exact content to return.

What does static mean?

The word “static” sounds limiting. Really, the word “static” should only be used to describe how the resulting Web site is deployed. This does not mean that you cannot create data-driven Web sites, or that you cannot provide dynamic experiences to users. There just isn’t any dynamism on the backend, per se.

Much of the dynamism on the Web today comes from the client-side, taking place directly in the browser using JavaScript and CSS. Client-side frameworks like AngularJS and Ember help develop rich user experiences, and other JavaScript frameworks make it easy to integrate dynamic content like maps (e.g. Leaflet).

Jekyll also supports data files that can be parsed and used during content generation. These data files can be created by hand, by tools, or generated from databases. But keep in mind that when the data changes, the Web pages that serve that data need to be re-generated.

Finally, using Jekyll to generate the content of a Web site or application does not preclude using a separate companion backend application to handle data storage and management, e.g. via an HTTP-based API. I have not tried it myself, but this setup could provide for a nice separation between Web page content and backend data services.

To create and build your first Jekyll Web site is easy:2

> jekyll new lovelysite.com
> cd lovelysite.com
> jekyll build 

The generated site will end up in a directory called _site.

Deploying with Git

Jekyll is a very interesting and powerful framework, but I really wanted to discuss deployment scenarios using Git. The basic idea we want to achieve is the following:

  1. Keep all content version controlled using Git
  2. Keep a remote Git repository on the deployment server
  3. To re-deploy updated content, simply push changes to the remote Git repository

Git has two kinds of repositories: “bare” and “non-bare”. The main difference is that bare repositories do not contain a working directory. That is, you cannot directly see the files that are part of that repository.3 Another minor difference is the convention that the name of the root directory of bare repositories ends with .git. It is not recommended that you push changes from a non-bare repository to another non-bare repository, because it already contains a working directory and it can cause issues unless you know what you are doing.

We want to have:

  • A bare repository on the deployment server
  • A non-bare clone of the repository on our local machine where we do work

We login to our remote server to setup our bare repository:

remote> mkdir -p ~/git/lovelysite.com.git
remote> cd ~/git/lovelysite.com.git
remote> git init --bare --shared .

Notice the .git ending on the directory name. Then on our local machine:

local> mkdir -p ~/git/lovelysite.com
local> jekyll new ~/git/lovelysite.com
local> cd ~/git/lovelysite.com
local> git init . 
local> git add .
local> git commit -m "first release"
local> git remote add origin user@remoteserver.com:~/git/lovelysite.com.git
local> git push --set-upstream origin master

For the non-initiated Git user, this might seem like a lot of strange commands. But we did the following: We created an empty bare Git repository on the remote machine to hold the content of our site. We then created the initial content on our local machine using Jekyll (it creates sample pages that we can modify later). We made the Jekyll site a non-bare Git repository, added all the files in the current directory to be committed, and then committed them using a simple commit message: “first release”. Next we told Git about our remote Git repository, found at ~/git/lovelysite.com.git on the machine remoteserver.com that we can access using the user user. In the last step we pushed our first commit to the remote server (--set-upstream tells Git to remember the link between our local repository and the remote one).

Automatic deployment with Git hooks

Almost done. The last step is to configure the remote repository to re-build the entire Jekyll site when new content is pushed into that repository. This is done using Git hooks. Hooks are just scripts that are run at certain times in the Git workflow process. We will use the post-receive hook that runs after Git has done all its work. For bare Git repositories, hooks are stored in a folder called hooks in the root repository directory. Make sure that there is a file called post-receive with the following contents and that it is executable.

remote> cat ~/git/lovelysite.com.git/hooks/post-receive 
#!/bin/sh

GIT_REPO=$HOME/git/lovelysite.com.git
TMP_GIT_CLONE=$HOME/tmp/lovelysite.com
PUBLIC_WWW=$HOME/webapps/lovelysite

git clone $GIT_REPO $TMP_GIT_CLONE
jekyll build -s $TMP_GIT_CLONE -d $PUBLIC_WWW
rm -Rf $TMP_GIT_CLONE
exit

remote> chmod 755 ~/git/lovelysite.com.git/hooks/post-receive

The assumption here is that $HOME/webapps/lovelysite is the location where your Web provider tells you to put the contents of your site.

Now, when we push new content, the post-receive script is run. The script creates a temporary clone of the repository with a working directory, and then uses that to build the site. Once the site is built and deployed, the temporary clone is removed.

With this setup, your deployment strategy becomes:

local> git add [files to update]
local> git commit -m "added new content"
local> git push 

It doesn’t get much more simple than that. Revision control and deployment strategy closely integrated.4

  1. There are many Web application frameworks available, in a range of different languages and with different architectural styles and patterns.

  2. Jekyll first has to be installed and the jekyll binary has to be on your $PATH. See Jekyll’s Web site for instructions.

  3. There is a relationship between bare and non-bare repositories. Each non-bare repository has a .git folder that contains an object database with all the revisions, along with other metadata and scripts. Next to the .git folder are all the files in the working directory. A bare repository on the other hand does not have a working directory, instead all the files in the .git directory in a non-bare repository are in the root repository directory.

  4. In an ideal world, you also want to have a non-deployment remote repository that you can push to without causing a re-deployment. You can for example host this on GitHub or in your Dropbox folder. Make this remote repository your default remote repository, and name your remote deployment repository deploy. Now your deployment strategy becomes: git push deploy master.