Déployer Django en production avec Nginx, Gunicorn et Supervisor

Django, c'est bien. Par contre, déployer un projet Django en production, ce n'est pas toujours évident, surtout que la doc n'est pas forcément toujours très à jour à ce sujet.

La dernière fois que j'ai dû effectuer un déploiement bien propre en production, j'ai un peu regretté de ne pas avoir sous la main un beau tuto bien récent, bête et méchant. Comme j'ai dû rédiger la doc complète de l'opération, en voici la version française.

Au menu : du Nginx en frontal et reverse proxy vers Gunicorn qui sert notre projet tournant dans un virtualenv (foutaises !). On y va ?!

Installer les paquets nécessaires

Que vous passiez par de l'hébergement mutualisé, un conteneur OpenVZ, une instance EC2 ou autre, je considère que vous démarrez avec un système Debian stable vierge.

Commençons par installer les paquets nécessaires (et supprimer l'inutile).

apt-get update
apt-get upgrade
apt-get purge apache2
apt-get install build-essential libpq-dev python-dev
apt-get install postgresql postgresql-contrib nginx git supervisor

Installer Node

Ok, ok, ça peut sembler bizarre de commencer par installer Node.js. le fait est que j'utilise l'excellent django-pipeline pour gérer la compression des assets, qui lui même utilise yuglify pour la minification du js. Et le meilleur moyen d'installer yuglify, c'est encore avec npm.

cd /opt/
wget http://nodejs.org/dist/latest/node-<latest_version>-linux-x64.tar.gz
tar -zvxf node-*.tar.gz
cd node-*
./configure
make
make install

Installons yuglify dans la foulée.

npm install -g yuglify

(Je sais, je sais, c'est vraiment sortir la Grosse Bertha pour tuer une mouche. Passons.)

Création de la base de données

J'utilise PostgreSQL pour gérer les données. Tout autre choix devra être justifié par une longue dissertation de 10 pages. En attendant, nous allons créer l'utilisateur et la BD.

su - postgres
createuser -P

    Enter name of role to add: monprojet
    Enter password for new role: monprojet
    Enter it again: monprojet
    Shall the new role be a superuser? (y/n) n
    Shall the new role be allowed to create databases? (y/n) n
    Shall the new role be allowed to create more new roles? (y/n) n

createdb --owner monprojet monprojet

Installer Python

C'est parti pour l'installation de notre langage préféré. On commence par pip et virtualenv:

cd
wget https://raw.github.com/pypa/pip/master/contrib/get-pip.py
python get-pip.py
pip install virtualenv virtualenvwrapper

Créons l'utilisateur système:

adduser monprojet --disabled-password
su - monprojet

Ajoutons ces lignes à la fin du fichier ~/.profile:

export WORKON_HOME=~/.virtualenvs
mkdir -p $WORKON_HOME
source `which virtualenvwrapper.sh`

Puis:

source ~./profile

Installer l'application

Création du virtualenv, git clone, etc. Que du classique, les chemins et urls seront bien entendu à adapter en fonction de la configuration de votre projet.

cd
mkvirtualenv monprojet
git clone https://git.monprojet.com/monprojet.git monprojet
cd monprojet
pip install -r requirements/production.txt
cd src
export DJANGO_SETTINGS_MODULE=core.settings.production
python manage.py collectstatic
python manage.py syncdb

Voici un exemple de fichier requirements/production.txt:

-r base.txt

gunicorn==18.0
psycopg2==2.5.2

Faire tourner l'application

Nous allons utiliser Gunicorn, un serveur d'application WSGI développé en Python. C'est lui qui va effectivement servir notre application. En fait, Gunicorn est déjà installé, puisqu'il était listé dans le fichier requirements.txt. Si ce n'était pas le cas, toutefois :

pip install gunicorn

Django fournit directement une intégration à Gunicorn, aucune autre opération n'est donc nécessaire. C'est presque de la triche tellement c'est simple. Il faut évidemment activer l'application adéquate dans votre fichier de settings:

INSTALLED_APPS += (
    'gunicorn',
)

Configurer Supervisor

Supervisor est un outil qui permet de contrôler les processus qui doivent tourner sur notre machine. Nous l'utilisons ici pour démarrer Gunicorn. Créez un fichier de config dans /etc/supervisor/conf.d/monprojet.conf. Voici un exemple fonctionnel :

[program:monprojet]
environment=DJANGO_SETTINGS_MODULE='core.settings.production'
directory=/home/monprojet/monprojet/src
command=/home/monprojet/.virtualenvs/monprojet/bin/python manage.py run_gunicorn
user=monprojet
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/monprojet.log
redirect_stderr=true

On lance tout ça :

supervisorctl reread
supervisorctl reload

Note : ces deux commandes seront à retaper en cas de modification de la conf de Supervisor. Notez que par défaut, run_gunicorn lance gunicorn sur le port 8000 en localhost.

Configurer le serveur web

Passons à la configuration du serveur web. Je recommande fortement Nginx qui allie souplesse, légereté et puissance. Une fois qu'on y a goûté, difficile de revenir vers Apache et autres éléphants (et non, ce billet n'est pas sponsorisé).

D'abord, je suppose que notre projet Django est le seul virtual host servi. On va donc modifier le virtual host par défaut dans /etc/nginx/sites-available/default.

server {
        listen 80 default_server;
        return 444;
}

Cette configuration signifie que tout ce qui ne tombera pas sur un virtual host bien défini (accès par l'url directe, par exemple) sera pûrement rejeté par Nginx.

Nous allons ensuite créer un virtual host spécifique pour notre projet dans /etc/nginx/sites-available/monprojet.

upstream monprojet {
    # En supposant que le serveur WSGI tourne sur le port 8000
    server localhost:8000;
}

# Redirection de monprojet.com vers www.monprojet.com
server {
    server_name monprojet.com
    return 301 $scheme://www.monprojet.com$request_uri;
}

server {
    server_name monprojet.com
    access_log /var/log/nginx/monprojet.access.log;
    error_log /var/log/nginx/monprojet.error.log;

    location /static/ {
        alias   /home/monprojet/monprojet/public/static/;
    }

    location /media/ {
        alias   /home/monprojet/monprojet/public/media/;
    }

    location / {
        proxy_pass http://monprojet;
        proxy_redirect off;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Nous adoptons ici une configuration en reverse-proxy. Nginx est utilisé en serveur frontal, pour servir les fichiers statiques et balancer la connexion au serveur WSGI. Vous allez voir, ça poutre des loutres.

N'oublions pas de créer le lien symbolique pour l'activer :

ln -s /etc/nginx/sites-available/monprojet /etc/nginx/sites-enabled/

Ni de redémarrer Nginx :

/etc/init.d/nginx restart

Et voilà !

Et vous voilà avec une configuration qui tronçonne du castor, capable d'encaisser un bon paquets de requêtes et de scaler sans trop de soucis. Était-ce vraiment si compliqué ? Non, non, ne me remerciez pas.

à++;