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 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.
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
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:
- Keep all content version controlled using Git
- Keep a remote Git repository on the deployment server
- 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 .
.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 firstname.lastname@example.org:~/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
There is a relationship between bare and non-bare repositories. Each non-bare repository has a
.gitfolder that contains an object database with all the revisions, along with other metadata and scripts. Next to the
.gitfolder 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
.gitdirectory in a non-bare repository are in the root repository directory. ↩
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. ↩