One-click App Deployment with Server-side Git Hooks
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.
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:
- A working repository
- A remote, bare, repository
- 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
#!/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:
- Update the code in the deployed directory, to the latest version
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.
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,
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.
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:
With that in place, all we need to do is then start developing our application and push as required.
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.