Back home (Thomas Pelletier)

Git-based Django deployment

Published on April 13, 2011.

If you ever used Heroku (or any similar service), you may be jealous of their straightforward way to deploy new versions of applications. At least, I am.

That’s why I came up with a couple of hacks to mimic their git-based deployment workflow, but for my own Django applications, served by Gunicorn.

The basic idea is: your application code is contained in a Git repository, and your production deployments are as simple as a git push.

Note: because we will type commands on both the client and server side, server-side commands will start by a > and client-side commands by a $. All commands are executed by a regular user.

Table of contents

Prepare your server

First of all, let’s prepare the ground by installing all the software we need on our production server (for the record, I used a Lucid Vagrant box):

> sudo apt-get install git-core
> sudo apt-get install python-virtualenv
> sudo apt-get install nginx

Now that we have the software, let’s create the base structure for our production website. In my configuration, I’m in my home, which is /home/vagrant/.

> mkdir www
> cd www
> virtualenv --no-site-package .
> mkdir var

The var directory will contain the pidfile and the gunicorn socket file. You could also use it to store a sqlite database if you are running a test server for example.

Create your simple Django application

Switch to your desktop shell and create a basic Django application (I call it codebase) along with a git repository:

$ django-admin.py startproject codebase
$ cd codebase
$ git init .

We also add the Pip requirements file (because you use Pip to manage your module dependencies right?): $ echo "django\ngunicorn" > requirements.pip.

The deployment script

Now we have to create a simple bash script which will be executed right after you push your new code to the server. I put it in the root of the project tree because I think it’s a good thing to versionize it with git. So open your favorite text editor and write the deploy.sh script:

#!/bin/sh

# Remember that this script will be executed by the unix user who push to the
# git repository ; and the script will be executed in ~/www/codebase/.

# Update requirements
../bin/pip install -r requirements.pip

# Reload the Gunicorn instance
kill -HUP `cat ../var/pid`

# Ideas of other things to do:
#   Apply South migrations?
#   Clear cache?

Now we add a first commit to our local repository:

$ git add .
$ git commit -a -m "bare django project"

Tweak your production git repository

First, clone your local repository into the codebase direcory:

> git clone /vagrant/codebase /home/vagrant/www/codebase

Note: /vagrant/ is a mount point to my desktop machine.

In order to make git do the automatic things, we have to tweak its server-side configuration. So add the following to the > codebase/.git/config file:

[receive]
    denyCurrentBranch = "ignore"

It prevents Git from moaning about the fact that you push to a non-bare repository.

We also have to create a git hook so as it runs the deploy.sh script we wrote earlier. Write the following in the > codebase/.git/hooks/post-receive:

#!/bin/sh

# update the source tree
cd ..
env -i git reset --hard

# execute the deploy.sh script
sh deploy.sh

Don’t forget to > chmod +x codebase/.git/hooks/post-receive.

Unleash the green unicorn

In a normal production use, you should run Gunicorn using supervisord or anything like it. But for this example, I will run directly Gunicorn from the command line:

> bin/gunicorn_django --pid var/pid --socket var/socket codebase/settings.py

Add your changes to the production repository

Now you can easily push your changes to the production site. Just edit some files, commit, and git push ssh://yourserver/www/codebase/.git master and you’re done. Requirements will be installed and Gunicorn will be gracefully reloaded.

Go further

The configuration is not done yet: we don’t take care of settings! We obviously need to swap development settings with production settings. You can use my Rails-like configuration for Django to do this (it works flawlessly).