One-click App Deployment with Server-side Git Hooks

Synopsis

This post shows the user how to deploy a simple website using no more than a git post-receive hook. It covers just enough background information so that the reader can go further and expand their knowledge as their time permits.

One-Click Deploy

How often do we say preach the mantra of one click deployment? Every Project? Every Day? Every Commit? If we’re anything alike, you’ll have readily identified with one or more of these. We've all been in roles or had projects where we've had to jump through a number of hoops to get our application deployed on one or more environments.

We know that ultimately it's a distraction from what we're best at: application design and development. We know we should be focused on other areas, such as architecture, testing, and design. Yet we get repeatedly bogged down in the minutiae of copying files, running scripts and so on. We get repeatedly bogged down in tasks which should always be automated.

Perhaps you're familiar with one of the following scenarios:

  • Following a deploy checklist
  • SSH'ing to a remote box and running a set of scripts
  • Manually FTP'ing files to a remote server

I hope you're not doing the last one…

For years I talked the talk but didn't walk the walk. But in recent times, my life, my time, my family and friends became too important to continue that way. They became too important to waste time, spending countless hours, deploying code. Perhaps you're the same. If you are, this article series is for you.

Recently I read Tim Boronczyk’s Git Hooks for Fun and Profit post here on Sitepoint and thought what if. I read the manual on using git hooks and took inspiration from Tim's post, thinking that there must be a way to automate deployment of a remote web app with them.

I set myself a simple, clear, goal:

I will only need to run git push and the deployment will be taken care of, with no further involvement from me.

Friends looked at me a bit dubiously when I talked about it; but I was inspired. Justifiably, at this point, you may be wondering, why not just use a Continuous Integration (CI) server, such as Jenkins or Hudson, or a service like codeship.io? That's a good question, and if you thought it, points for doing so. Many don't.

Why not use one? Well as Beth Tucker-Long said in her talk at PHPUK last year, not all needs are so complex or sophisticated, or can justify the implementation costs.

So in today's post, we’re starting a series looking at how git hooks can help you implement simple and effective deployment processes. Today’s is the simplest of the lot.

How Will It Work?

It consists of 3 parts:

  1. A working repository
  2. A remote, bare, repository
  3. A working directory (your deployed site or test branch)

We'll develop our application as normal, in our local working directory, and push to a remote as we normally would (e.g. Github, BitBucket etc). The remote repository though won't be a standard development repository. It needs to be what's called a bare repository, which the git scm book describes as:

a repository that doesn’t contain a working directory

If this seems a little confusing, that's perfectly normal. I did a lot of reading as I came to understand this setup. During that research I found this quote from bitflop.com which explains it quite succinctly.

In Git you should only use a "bare" repository to clone and pull from, and push to. It doesn't have a checked out tree, so it just does what the "server" notionally does in a centralized VCS – records commits, branches, etc when you push to it, and gives you the latest versions when you clone or pull from it.

So in our bare repository we won't be able to edit files and make changes. It's purely there for us to push our latest changes to and for those changes to be deployed from, to our working directory. If it helps, use the image below to conceptualize it.

Setting Up the Remote Repository

For the purposes of this article, I'm going to assume you're using a Linux (Debian) server, and that the remote repository will be stored in /opt/git. We'll call it simpleapp.git. I've added .git at the end because it seems to be the accepted convention.

Using these assumptions, we run the following commands, to setup the remote repository:

$ mkdir -p /opt/git
$ cd /opt/git
$ git clone --bare simpleapp simpleapp.git
Initialized empty Git repository in /opt/git/simpleapp.git/

When this is finished, if you ls the directory, you'll notice that it contains what would normally be stored in the .git directory. This is perfectly fine as the working directory in this case is, effectively, the deployed directory.

The Post-Receive Hook

With the remote repository setup, we next add a git post-receive hook. I've supplied a sample one in post-receive.sh:

#!/bin/sh
# 
## store the arguments given to the script
read oldrev newrev refname

## Where to store the log information about the updates
LOGFILE=./post-receive.log

# The deployed directory (the running site)
DEPLOYDIR=/var/www/html/simpleapp

##  Record the fact that the push has been received
echo -e "Received Push Request at $( date +%F )" >> $LOGFILE
echo " - Old SHA: $oldrev New SHA: $newrev Branch Name: $refname" >> $LOGFILE

## Update the deployed copy
echo "Starting Deploy" >> $LOGFILE

echo " - Starting code update"
GIT_WORK_TREE="$DEPLOYDIR" git checkout -f
echo " - Finished code update"

echo " - Starting composer update"
cd "$DEPLOYDIR"; composer update; cd -
echo " - Finished composer update"

echo "Finished Deploy" >> $LOGFILE

This script, written in bash, will be fired off after the push is received by the remote repository. It manages the entire deployment process for us, consisting of the following steps:

  1. Update the code in the deployed directory, to the latest version
  2. Run composer install, ensuring we have all dependencies in place

Let's look at it piece by piece:

read oldrev newrev refname

Here we store the three arguments passed to the post-receive script when it's called. These are:

  • The previous commit SHA1 hash
  • The latest commit SHA1 hash
  • The refname (containing branch information)

This helps track what's being deployed and to rollback should you need to do so. This script doesn't handle a rollback however.

LOGFILE=./post-receive.log
DEPLOYDIR=/var/www/html/simpleapp

Next we setup a variable to log the output of the deployment and the deployment directory.

echo -e "Received Push Request at $( date +%F )" >> $LOGFILE
echo " - Old SHA: $oldrev New SHA: $newrev Branch Name: $refname" >> $LOGFILE
echo "Starting Deploy" >> $LOGFILE

Here we're logging when a push request was received and the details about it. That way, should we need to debug, we have relevant information to hand and a quick message to say that we're starting the deploy process.

echo " - Starting code update"
GIT_WORK_TREE="$DEPLOYDIR" git checkout -f
echo " - Finished code update"

Here we tell git where the working tree is, and instruct git to checkout the latest copy of the code to that directory, removing any changes which may have manually been made.

echo " - Starting composer update"
cd "$DEPLOYDIR"; composer update; cd -
echo " - Finished composer update"

Now that the code's updated, as I use composer for dependency management, we run composer update as there may have been a change to composer.json changing the application's dependencies.

echo "Finished Deploy" >> $LOGFILE

The last step is indicating that the deploy's now complete. You'd likely do a lot more than this in a standard project, including:

  • Clearing and pre-warming cache directories
  • Running database migrations
  • Running unit & regression tests
  • Checking permissions on key directories

However, this is a simple example and I don't want to distract you from the main aim. In the hooks directory, create a new file called post-receive and add the contents of post-receive.sh. You should only need to change one variable, DEPLOYDIR.

Change it to suit your needs, and don't forget to make the file executable, otherwise it won't run. If you're not familiar with how to do that, it's as follows:

chmod +x post-receive

The Git User

Whilst on the remote server, there's one step left to go, the git user. This is the user who will manage the process, and who we'll authenticate as. So if it doesn't already exist, run the following command to create it:

sudo adduser git

After that, you need to add your public key to the authorized_keys file in the .ssh directory in the git user's home directory.

Note: If you don't have one or aren't familiar with them, this excellent, short guide from GitHub steps you through creating one.

The simplest way to copy the public key is likely copying the contents between terminal sessions. So copy the contents of ~/.ssh/id_rsa.pub whether via scp or terminal session to your remote server and add them to the remote authorized_keys file.

Then from your local machine, test that you can now ssh to the remote box by running the following command, substituting remoteserver as appropriate.

ssh git@remoteserver 

All being well, you're not prompted for a username and password. Next, ensure that the git user has proper permissions on both the /opt/git/simpleapp directory and the deploy directory.

Adding The Remote Repository

After that, we're pretty much finished. In your working directory, run the following command to add the new remote repository:

git remote add test git://remoteserver/opt/git/simpleapp.git

Then, verify that it's in place with the following:

git remote

With that in place, all we need to do is then start developing our application and push as required.

In Conclusion

I understand that it's a lot to take in as it's quite an involved process, at least at first. However, like Linux, once you've got everything up and running, the majority of the work's over.

What do you think? Let me know what else you'd like to see. I hope this article's shown you that it's both possible and straight-forward to auto-deploy.

Have you already done this? Have you approached it a different way? Can you do better? Give me your thoughts in the comments.

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • kkerley

    This is awesome! Thank you for writing this up. I’m a Rails developer and have sadly been doing my little pet projects on my Digital Ocean VPS using an FTP client to manually move files. :( It has been painful. I’ve been trying to get Capistrano up and running and ultimately, that’d probably be the best solution, but this is an awesome stop-gap assuming it’ll work the same with Rails apps.

    Thanks again!

    • http://www.matthewsetter.com/ Matthew Setter

      @kkerley:disqus I’d suspect that it would work, though I’m only a hobbyist Ruby developer. Feel free to write more if you need to. Let’s get you sorted.

  • ryan_laneve

    Very good article. My end-goal was pretty much the same – “git push and be done” – but I’m relying on an existing project to take care of pretty much everything for me: dokku (https://github.com/progrium/dokku). Once setup, you’ve basically got a personal, mini-Heroku running. I’ve got it setup on a Digital Ocean VPS, running one rails app, two sinatra apps, a node app, and a shared postgreSQL instance, and it’s been fantastic.

    • http://www.matthewsetter.com/ Matthew Setter

      @ryan_laneve:disqus there’s a few more parts coming, where I’m looking at slightly more complex setups. I hope that they can help you out more specifically. Thanks for sharing your thoughts too.

  • Ignatius Teo

    Post-receive FTW! That’s how we have it set up here on both staging and prod. :)

    • http://www.matthewsetter.com/ Matthew Setter

      Yeah, now that I’ve been using it on some freelance projects, I can’t believe I used to do it any other way. It’s becoming my base standard. Any tips for taking it further @ignatiusteo:disqus

  • João Goulart

    On Fedora 19:
    git clone –bare simpleapp simpleapp.git
    fatal: repository ‘simpleapp’ does not exist

    • http://www.matthewsetter.com/ Matthew Setter

      Hey João, I’ll have a look and see what’s happened there. Thanks for letting me know.

    • http://acooze.co/ Michael Kimpton

      +1 on Ubuntu 12

      git clone –bare simpleapp simpleapp.git
      fatal: repository ‘simpleapp’ does not exist

      Worked well on Redhat.
      Thanks Matt, great article.

      • http://www.matthewsetter.com/ Matthew Setter

        Not a worry Michael. Thanks for the feedback.

  • http://www.satishsays.com/ Satish S

    Or use Capistrano

    • http://www.matthewsetter.com/ Matthew Setter

      @satishsays:disqus, well, yes you could. But this is designed to be a simple look at one-click deployment techniques. I quite like Capistrano fwiw. Have you tried Webistrano?

  • Mantas Urnieža

    It looks very OK, but I do not see how it is easier than some kind of CI (lets say Jenkins) + Capistrano.

    • http://www.matthewsetter.com/ Matthew Setter

      @mantasurniea:disqus , it’s not necessarily easier for the initial setup and it’s not intended to be a replacement for Jenkins &/or Capistrano. But it’s another, simple, way of approaching deployment.

  • Phil Birnie

    HI Matthew; thanks for the helpful post. Should chmod +x post-release be chmod +x post-receive?

    • http://www.matthewsetter.com/ Matthew Setter

      Yes, yes it should. Thanks for spotting that.

      • http://www.matthewsetter.com/ Matthew Setter

        @philbirnie:disqus – that’s now been updated in the article.

  • http://www.matthewsetter.com/ Matthew Setter

    Hey @tlongren:disqus thanks for the link mate. That’s beyond cool and I have to try it out. Really appreciate you shared it. WOW

  • adri_p

    Great article! But don’t you mean you do a `composer install` instead of a `composer update`?

    • http://www.matthewsetter.com/ Matthew Setter

      @adri_p:disqus yes indeed. It’s now been updated in the article. Thanks for spotting and mentioning that.

  • Phil Birnie

    Matthew – everything seems to be working well, but I noticed that within my working directory, any files that are added show up as untracked files when running a `git status.` Any changes show as `modified` Did I do something wrong? In essence, it looks like the changes are pushed over, but the git repo is not staying up to date. I can resolve this with git –reset hard origin master, but I’m assuming that’s not what I should be doing..

    • Phil Birnie

      Never mind – my issue was that I was using an existing repo – I didn’t realize that you have to remove the .git folder in the working directory! In retrospect, that makes perfect sense!

  • Errol Sayre

    I like the simplicity of having a discrete repo for test and production, however I already took the time to build a single deployment script that works for all my gitolite repos. In it I use anything master branch for testing and accept tags for sending to production. Granted, in my approach I only have the two “destination types”.

    • http://www.matthewsetter.com/ Matthew Setter

      @errolsayre:disqus is it available in a Gist at all? I’d be keen to have a look.

      • Errol Sayre

        When I first set this up I didn’t think to do it as a gist but it is in a repo: https://github.com/ErrolSayre/SimpleDeploy

        This doesn’t have tags included, but I can look at migrating that into this if you need that.

  • http://www.matthewsetter.com/ Matthew Setter

    @m1gg:disqus hmm, that’s a good idea. I’ll do some digging and see what I can find.