blog

Graphic of Git Logo

Deploying Websites with Git

by

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.

Next step…

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."

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

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? 🙂

+ more