Push-to-deploy with Git

Automating your deployment with Git Hooks

In the past, deploying changes to production was as simple as uploading the edited files with an FTP client. But with the modern complex applications we build for the web today, often as part of a distributed team, deploying to production in this manner simply isn't fit for purpose.

So what's the solution? Spend thousands of dollars on a deployment solution? No. Chances are if you had that sort of cash then you would already be doing that.

If you already use Git as your version control software, we can leverage some of Git's more powerful and perhaps lesser used features to automate deployments to a remote server.

In this article I will introduce you to Git bare repositories and Git hooks, specifically the post-receive hook, and show you how you can use them together to automate deployment to a remote server.

Prerequisites

This article assumes you're already familiar with the basics of Git and how to use it via the terminal/command line. This article also assumes you're familiar with basic terminal commands and how to access a remote server via SSH.

The instructions in this article are for a remote server running on Ubuntu 14.04, and a local dev machine running Mac OSX Yosemite 10.10.3, but they should also work for other OS's with a little tweaking. Both environments should already have Git installed on them, if they do not, please install Git before continuing.

Finally, please ensure you make backups of your data before following this guide. I accept no responsibility for any loss of data, misconfiguration or damage otherwise caused. Sigh, it's a sad world we live in when we have to cover ourselves legally, but there you have it. Right, on with the guide!

Introduction to Git hooks

Git hooks are event driven, when certain events happen as a result of an action, such as pushing code, Git checks a series of hooks to see if there is an associated script to run.

Some scripts run prior to an action taking place, which can be used to ensure code compliance to standards, for sanity checking, or to set up an environment. Other scripts run after an event in order to deploy code, re-establish correct permissions (something git cannot track very well), and so forth.

Using these abilities, it is possible to enforce policies, ensure consistency, and control your environment, and even handle deployment tasks.

Git post-receive hook

This is run on the remote when pushing after the all refs have been updated. It does not take parameters, but receives info through stdin in the form of <old-value> <new-value> <ref-name>. Because it is called after the updates, it cannot abort the process.

What this allows us to do is for us to git push our local changes to production, and have Git on the server handle the deployment for us through a script that checks out the changes.

Git bare repository

Since we will never be directly editing the files checked out on the remote server, and nor shouldn't we, we will be using a bare repository instead of a working repository.

What's the difference between a bare repo and a working repo?

Jon Saints sums this up nicely in his article 'What is a bare git repository?':

Repositories created with the git init command are called working directories. In the top level folder of the repository you will find two things:

  • A .git subfolder with all the git related revision history of your repo.
  • A working tree, or checked out copies of your project files.

Repositories created with git init --bare are called bare repos. They are structured a bit differently from working directories:

First off, they contain no working or checked out copy of your source files. And second, bare repos store git revision history of your repo in the root folder of your repository instead of in a .git subfolder.

So in essence, a working repository is for working, and a bare repository is for sharing.

The way in which we will be using a bare repository is by configuring Git to store the Git related revision history in a not-publicly web accessible folder, as this most likely contains sensitive information, and to checkout our project files into a separate publicly web accessible directory so that are project can be viewed.

Configuring the remote server for deployment

Now that we understand a bit more about Git hooks and Git bare repositories we can start to configure our remote server for push-to-deploy deployments.

Initialise a Git bare repository

Start off by SSHing into your remote server and creating a directory in your user directory. This directory will be used to hold our git bare repository:

$: mkdir ~/myproject.git
$: cd ~/myproject.git
$: git init --bare

Note: the '.git' suffix to the folder name is just a convention and is not necessary.

This initialises the bare repository, and all of the files that would otherwise be stored in a .git hidden folder in a working repository are stored at the root of this folder.

Create a post-receive hook

Next we need to setup the script that will run as part of the post-receive hook. Open up the following file in a text editor:

$: vim hooks/post-receive

So what do we need this script to do? When we push changes to the remote we want Git to checkout those changes to a publicly accessible folder in our web server's web root.

We can achieve this by adding the following lines:

#!/bin/bash
git --work-tree=/var/www/myproject --git-dir=/home/matt/myproject.git checkout -f  

Let's look at this command in more detail:

  • --work-tree flag: specifies the directory to checkout our project's working files to, in this case a publicly accessible folder in the Apache web root: /var/www/myproject.
  • --git-dir flag: specifies the git directory path for the project, in this case the bare repository we created in the last step in our user directory: /home/matt/myproject.git

This is great, but if we left the script as it currently stands, any branch that is pushed to the remote will be checked out. We need to add some logic to ensure that the only branch that can be pushed is master (or what ever branch you have designated as representing what is in production).

Fortunately as we learnt earlier, Git provides information about the change through stdin in the form of <old-value> <new-value> <ref-name>. We can use this to determine the reference for the change, ie: that it was the master branch that was pushed:

Add the following logic check to the post-receive hook:

#!/bin/bash
while read oldrev newrev ref  
do  
    if [[ $ref =~ .*/master$ ]];
    then
        git --work-tree=/var/www/myproject --git-dir=/home/matt/myproject.git checkout -f
    fi
done  

Let's break down what is going on in this code. First we read the stdin with a while loop, receiving three pieces of information (old rev, new rev, ref), separated by white space, for each ref.

Next we check for a master branch push by checking the $ref object for a reference that contains something like refs/heads/master via the if logical check.

This ensures that only changes being pushed from the master branch to the remote will be applied.

Seeing as for server-side hooks Git is able to pass messages back to the connected client, we will add some stdout to inform the user of what is happening:

#!/bin/bash
while read oldrev newrev ref  
do  
    if [[ $ref =~ .*/master$ ]];
    then
        echo "Master ref received.  Deploying master branch to production..."
        git --work-tree=/var/www/myproject --git-dir=/home/matt/myproject.git checkout -f
    else
        echo "Ref $ref successfully received.  Doing nothing: only the master branch may be deployed on this server."
    fi
done  

That's it for the script. Save and close when you're finished.

Now that we have written our post-receive hook we need to make it executable so that Git can run it:

$: chmod +x hooks/post-receive

Configuring our local machine

On your development machine change directory into your project's working directory:

$: cd /path/to/myproject

Next we will add a new remote to git called production. For this you will need to know the following information:

  • The username that is used for your production server.
  • The IP address or domain for the production server.
  • The location of the bare repository you set up in relation to the user's home directory.

The command you'll run should look something like this:

$: git remote add production matt@server_domain_or_IP:myproject.git

That's it. You can now successfully deploy your master branch to your remote server by running the following command:

$: git push production master

Note: If you have not configured SSH keys, you may be prompted to enter your password.

Upon running the command above, you should see something similar to the following output in your terminal window:

Counting objects: 8, done.  
Delta compression using up to 2 threads.  
Compressing objects: 100% (3/3), done.  
Writing objects: 100% (4/4), 473 bytes | 0 bytes/s, done.  
Total 4 (delta 0), reused 0 (delta 0)  
remote: Master ref received.  Deploying master branch...  
To matt@xxx.xxx.xxx.xxx:myproject.git  
   009183f..f1b9027  master -> master

If we try to push a branch other than master to production, then you will see the following:

$: git checkout feature/test-branch
$: git push production feature/test-branch
Counting objects: 5, done.  
Delta compression using up to 2 threads.  
Compressing objects: 100% (2/2), done.  
Writing objects: 100% (3/3), 301 bytes | 0 bytes/s, done.  
Total 3 (delta 1), reused 0 (delta 0)  
remote: Ref refs/heads/feature/test-branch successfully received.  Doing nothing: only the master branch may be deployed on this server  
To matt@xxx.xxx.xxx.xxx:myproject.git  
   83e9dc4..5617b50  feature/test-branch -> feature/test-branch

Notice the remote's post-receive hook executed the else block on our logical check, resulting in no changes being applied.

Conclusion

As we have demonstrated in this article, it is possible to setup a sophisticated deployment mechanism with Git.

You can expand on the topics covered in this article further; adding remote post-receive hooks to other environments, allowing you to push code to a staging server for testing prior to pushing to production.

You can also extend this script to run other commands, such as installing package dependencies, or configuring the application prior to being deployed. The possibilities are endless.

The real benefit though is the automation this technique brings. As we have demonstrated we have removed some of the human error from the process, and streamlined what would otherwise be a tedious and repetitive task.

Matt Fairbrass's Picture
Matt Fairbrass

Matt is a UX developer & designer originally from London, England now living in Sydney, Australia. Matt has over 5 years professional experience building web & mobile apps using web technologies.

Sydney, Australia
Matt Fairbrass's Picture
Write a comment Previous article » Prev »