Deploying Websites with Git

Git-Logo-1788C

Deploying your webapp is an important part of the web development equation – your client’s site isn’t going to attract a lot of attention sitting in your local dev directory. Deployment concerns tend to fall to the bottom of the priority list, though, and the end result tends to be kludgy, hastily thrown-together deployment scripts; and because they are so kludgy and, often, time consuming, when time crunches threaten, a developer may resort to making changes directly on the remote server that need to be (but sometimes never are) backported to the code living in your version control.

Wouldn’t it be better if you could deploy your newest code with a single command, using the same tool you use for version control? One git push, and your site is serving your newest commits. No more kludgy, multi-part scripts, no more threat of overwriting critical fixes with the next deploy.

Let’s make that happen.

Process Summary

The key lies in git hooks, scripts that are triggered by certain git events (indicated by the name of the script itself), living in the $GIT_DIR/hooks directory.

The process, then, is:
1) Create a bare remote repository on our server;
2) Add this repo as a remote target for git;
3) Create a post-receive script to be run in the remote repo on receiving a git push;
4) This script will git checkout -f the newest code to a work tree;
5) Miscellany, depending on your specific needs (discussed in more detail below).

Remote Setup

Starting assumption: You have ssh access to your server.

1. In any directory you have write-access to (say /home/yourname) create a new directory. You can call it whatever you want, but for our purposes we’ll assume you named it deploy.git.
2. cd to this directory, and initialize a new bare repository.

$ mkdir deploy.git
$ cd deploy.git
$ git init --bare

Why a bare repository?

A bare repository in git acts like a centralized server. Instead of the main directory being the working tree, with the git files stores in a .git dir under said main directory, the main directory directly hosts the git files, and no working tree is present. Instead, it simply records commits, branches etc. when pushed to, and will return the latest versions when cloned or pulled from.
In git version 1.7+, a repo must be bare in order to accept a push.

3. Our next step will be to create the post-receive hook to checkout a branch to your server’s document root (ie. /var/www). You’ll likely want to set up a directory under this for your site if you expect to be serving multiple sites (ie. /var/www/mysite), and set up the virtual host accordingly. Virtual hosts are beyond this article’s scope.

$ cd hooks
$ vi post-receive
|i|
#!/bin/sh
set -x
GIT_WORK_TREE=/var/www git checkout -f
|esc|
|:wq|
$ chmod +x post-receive
If you want to force a particular branch to be checked out (say, develop), just specify it
at the end of the command as:
GIT_WORK_TREE=/var/www git checkout -f develop
Otherwise, you can expect that the branch we specify as master (below) will be the one getting checkout out.

I’m getting permission errors.

You need to ensure the user that the post-receive hook is running as has write permissions on the working tree directory (in this case, /var/www.

Why am I getting ‘bad default revision HEAD’ errors?

Potentially you haven’t commited anything to this repo yet. Otherwise, you need to tell git which branch you want it to check out – either perform an initial git checkout |branchname| or add the branch name to the end of the checkout command in the post-receive script.

Local Setup

Starting assumption: You have an already initialized git repo locally. If not, run git init in the top-level directory you want to track, and make an initial commit: git commit -a -m "Initial Commit.".

1. In your git repo directory, add your new remote repo to git, and give it a meaningful name (in this case, we’ll use staging).

$ git remote add staging ssh://yourname@example.server.com/home/yourname/deploy.git

2. Force push to the remote repo, using the HEAD of your current branch as the master. If you want to push a different branch, you can specify it with refs/heads/branchname where ‘branchname’ is the name of the branch, or you can just git checkout the appropriate branch.

$ git push staging +master:HEAD

You should receive output from the server detailing the successful execution of your script. If all went well, your files should now be checked out into the /var/www directory, with the git metadata remaining in your /home/yourname/deploy.git directory.

What if I have an existing directory structure I need to maintain? Or I don’t want all of the files in my repo sent to /var/www?

In this case, you’ll need to set a different git work tree and copy the directories/files you need independantly from said work tree directory to /var/www. You’ll want to create an additional directory (say, $mkdir /home/yourname/workdeploy that the files will be checkout out into, and then copy what you want across – this can all go into the post-receive hook. This could look like:

#!/bin/bash
set -x
GIT_WORK_DIR=/home/yourname/workdeploy
DOC_ROOT=/var/www

GIT_WORK_TREE=$GIT_WORK_DIR git checkout -f
rsync -rlD --delete --omoit-dir-times "$GIT_WORK_DIR/desired_subdirectory" $DOC_ROOT

I need to run sudo commands inside of post-receive; or, I’m receiving errors about ‘askpass’; or, I’m receiving errors about ‘requiretty’.

You’ll need to change some settings in your /etc/sudoers file in order to run commands on the server remotely if requiretty is set, and you’ll need to make further changes if you want to be able to run sudo commands remotely without needing to enter your password each time (potentially, you could also have a password agent running).
Note that the following reduces your system’s security by a certain degree (passwords are still needed for initial remote login).

$ sudo su - #to become root and inherit root path
$ visudo

Within visudo, you’ll need to add the following after the Default directive:
Defaults:yourusername !requiretty

Within visudo, you’ll also need to enter the following at the very end of the file to allow for passwordless execution of sudo:
yourusername ALL=(ALL) NOPASSWD:ALL

I assume that if you have sudo priviledges, you know better than to alter sudoers rashly. You can limit what can be executed passwordless with sudo if you know what specific commands or scripts you’ll need to run.

Updating

Alright, now that all the nasty bits are out of the way, how do we use this? From your local repo directory:

$ git push staging

Optionally, you can add the branchname to push to the end of that line. And that’s it. That’s the whole thing. Typing that updates your server with the newest commits.

It’s beautiful, ain’t it? 🙂

Christopher Keefer

Christopher Keefer

Christopher Keefer is a Senior Software Engineer at Art & Logic. He generally spends his spare time on the computer too, so there isn't much hope for him.
Christopher Keefer

Latest posts by Christopher Keefer (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

1 Comment

  1. Ron

    Great article. I recently started to migrate things over to Windows Azure. What’s awesome is that it has built in continuous integration. You can choose a repo from a number of different services and when you push to the branch of your choice, it will automatically push the build to Production.

    The advantage of doing it this way is that your developers can continue to work in a “development” branch or a Sprint based branch and only authorized users can push to Master. If you choose Master as your main code base that is a mirror of what’s on Production, then again, authorized users are only allowed access to merge development branches into Master. This is typically the role of a build/release manager… but you get the point.