Python/Django Continuous Deployment
This page describes a setup for (continuous) deployment of a Python/Django application.
Preface
An existing Python/Django application with the following structure:
myproject
|-- dependencies.pip
|-- static
|-- myproject
| |-- manage.py
| `-- ...
`-- venv
|-- bin
|-- include
`-- lib
- Git SCM with Github as central repository
One-time setup (server-side)
Python and virtualenv
$ apt-get install python python-pip python-virtualenv
Gitolite
See Gitolite.
The following two scripts are used to deploy and restart the python application after a git push
.
The script /srv/www/bin/deploy-python-app.sh
updates the application and its dependencies:
#!/bin/bash
set -e
APPNAME=$1
APPDIR=/srv/www/$APPNAME
cd $APPDIR
source venv/bin/activate
git reset --hard || true
git pull
pip install -r dependencies.pip
The script /srv/www/bin/restart-python-app.sh
restarts the python application:
#!/bin/bash
set -e
APPNAME=$1
/usr/bin/supervisorctl restart $APPNAME
In order to run the scripts the git user need sudoers rights. Add the follwing via visudo
:
git ALL=(root) NOPASSWD: /srv/www/bin/restart-python-app.sh, (www-data) NOPASSWD: /srv/www/bin/deploy-python-app.sh
Supervisor
$ apt-get install supervisor
The script /srv/www/bin/run-gunicorn-django.sh
is used by supervisor to start and watch the Django application.
#!/bin/bash
set -e
APPNAME=$1
PORT=$2
APPDIR=/srv/www/$APPNAME
LOGDIR=/var/log/gunicorn
LOGFILE=$LOGDIR/$APPNAME.log
USER=www-data
GROUP=www-data
NUM_WORKERS=3
cd $APPDIR
source $APPDIR/venv/bin/activate
test -d $LOGDIR || mkdir -p $LOGDIR
exec gunicorn_django $APPNAME \
-w $NUM_WORKERS -b 127.0.0.1:$PORT \
--user=$USER --group=$GROUP --log-level=debug \
--log-file=$LOGFILE 2>>$LOGFILE
exit 0
Per-project setup (server-side)
Create gitolite project
See also Gitolite.
Add to gitolite-admin/conf/gitolite.conf
:
repo myproject
RW+ = user1, user2, ...
R = www-data
Make sure the public key of www-data
is in gitolite-admin/keydir/
.
Create post-receive script /srv/gitolite/repositories/myproject.git/hooks/post-receive
:
#!/bin/sh
sudo -u www-data /srv/www/bin/deploy-python-app.sh myproject
sudo -u root /srv/www/bin/restart-python-app.sh myproject
And set execute bit:
chown git:git /srv/gitolite/repositories/myproject.git/hooks/post-receive
chmod 700 /srv/gitolite/repositories/myproject.git/hooks/post-receive
Setup web project
$ sudo su - www-data
$ cd /srv/www
$ git clone git@localhost:myproject
$ cd myproject
$ virtualenv --no-site-packages venv
Setup supervisor
Create /etc/supervisor/conf.d/myproject.conf
, :
[program:myproject]
directory = /srv/www/myproject
user = root
command = /srv/www/bin/run-gunicorn-django.sh myproject 8000
stdout_logfile = /var/log/supervisor/myproject.log
stderr_logfile = /var/log/supervisor/myproject.log
Reload configuration:
$ supervisorctl reload
Setup nginx
Create a config file /etc/nginx/sites-available/www-example-com
and create a symlink to /etc/nginx/sites-enabled
:
server {
server_name www.example.com;
server_name_in_redirect off;
access_log /var/log/nginx/www-example-com.access.log;
error_log /var/log/nginx/www-example-com.error.log;
location /static/ {
alias /srv/www/www-example-com/static/;
}
location / {
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_connect_timeout 10;
proxy_read_timeout 10;
proxy_pass http://127.0.0.1:8000/;
}
}
TODO: static
Push project:
$ git remote add prod git@server.example.com:myproject
$ git push prod master
Per-project setup (developer-side)
Install python and virtualenv
$ apt-get install python python-pip python-virtualenv
Setup a new project and virtualenv
Setup virtualenv:
$ mkdir myproject && cd myproject
$ virtualenv venv
$ source venv/bin/activate
Install dependencies:
(venv)$ pip install django
(venv)$ pip install gunicorn
(venv)$ pip freeze > dependencies.pip
Setup Django project:
(venv)$ django_admin.py startproject myproject myproject
Test Django server:
(venv)$ python myproject/manage.py runserver
Test Gunicorn server:
(venv)$ gunicorn_django myproject
From another console:
$ curl -i localhost:8000
HTTP/1.1 200 OK
Server: gunicorn/0.14.2
...
Setup git repo and push to github
Create a .gitignore
file:
venv
*.pyc
Create git repo, commit and push
$ git init
$ git add .
$ git commit -m "Initial commit"
$ git remote add origin git@github.com:...
$ git push origin master
Developer workflow
Clone the project and setup virtualenv
$ git clone git@github.com:...
$ cd myproject
Setup virtualenv:
$ virtualenv venv
$ source venv/bin/activate
Install dependencies:
(venv)$ pip install -r dependencies.pip
(venv)$ python manage.py runserver
Update dependencies
When new dependencies were added:
$ pip freeze > dependencies.pip
Deployment
$ git remote add prod git@server.example.com:myproject
$ git push prod master
Diagnostics
Test HTTP request to gunicorn:
$ curl -i locahost:8000
HTTP/1.1 200 OK
Server: gunicorn/0.14.2
...
Test HTTP request to nginx:
$ curl -i locahost:80
HTTP/1.1 200 OK
Server: nginx
...
Gunicorn should only listen on local interface:
$ netstat -tan | grep 8000
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN
Only the gunicorn parent process runs as root, workers run as www-data:
$ ps auxwww | grep gunicorn
root ...
www-data ...
www-data ...
www-data ...
TODOs:
- runit / supervisor / daemontools
- flup / uswgi / Apache+mod_wsgi
- gunicorn Debian package (backports) apt-get -t squeeze-backports install gunicorn
- git push vs. fabric
- DB updates?
- Tox: http://tox.testrun.org/
- Buildout
Sources: