Déployer Django avec Gunicorn dans un virtualenv

Il existe une multitude de manières de déployer un projet Django, certaines s'adaptant plus facilement que d'autres à votre infra. Voici comment j'ai procédé chez moi, en fonction de mes attentes.

Virtualenv

Afin de bien séparer chaque projet, nous allons utiliser un environnement virtuel pour chacun. Ainsi, deux projets nécessitant des versions de python différentes et/ou des dépendances dans des versions différentes pourons cohabiter, chacun étant cloisonné à son environnement.
Créons donc, dans le dossier ~/venvs/ un environnement portant le nom de notre projet, ici monprojet :

mkdir ~/venvs/
virtualenv ~/venvs/monprojet --no-site-packages

Notez que l'option --no-site-packages permet de commencer avec un environnement propre, sans aucun des paquets installés sur le système. Du coup, il nous faut y installer Django, Gunicorn et toutes les dépendances de notre projet :

source ~/venvs/monprojet/bin/activate
pip install django
pip install gunicorn

nginx

Afin de servir les fichiers statiques ainsi que ceux uploadés par les utilisateurs, nous utilisons un serveur web, ici nginx. La configuration est très simple :

server {
    listen         127.0.0.1:8080;
    server_name    example.org;

    root           /chemin/vers/le/projet;

    location / {
        deny       all;
    }

    location ~ /(static|media)/ {
        allow      all;
    }
}

Lancer Gunicorn

Afin de vérifier que tout fonctionne bien avec Gunicorn, on le lance manuellement depuis le virtualenv :

source ~/venvs/monprojet/bin/activate
cd /chemin/vers/le/projet
gunicorn --bind="127.0.0.1:8000" monprojet.wsgi:application

HaProxy

Il nous faut configurer HaProxy pour utiliser d'une part le serveur web afin de servir les fichiers statiques et d'autre part Gunicorn pour l'application elle même. Ceci est simplement une utilisation de plusieurs serveurs web sur une seule machine.

frontend http-in
    bind           *:80

    option         forwardfor header X-Real-IP

    acl            is_monprojet    hdr(host) -i example.org
    acl            is_static       path_beg /static /media

    use_backend    webserv         if is_monprojet is_static
    use_backend    gunicorn        if is_monprojet !is_static

backend webserv
    option         http-server-close
    server         webserv         127.0.0.1:8080 check

backend gunicorn
    option         http-server-close
    server         gunicorn        127.0.0.1:8000 check

On remarquera l'utilisation de l'option http-server-close qui empêche de keep-alive. Si vous l'oubliez, nginx prendra le dessus sur Gunicorn.

systemd

Le problème est que Gunicorn ne se lance pas tout seul. Si vous utilisez systemd, il est aisé de créer un service afin de gérer Gunicorn pour chacun de vos projets.
Commençons par créer le fichier /etc/conf.d/gunicorn_monprojet qui indiquera les informations nécessaires sur le projet :

PORT=8000
SRC_PATH="/chemin/vers/le/projet"
ENV_PATH="/home/utilisateur/venvs/monprojet"
APP_NAME="nom_de_votre_application"
ACCESS_LOGFILE="/chemin/vers/un/fichier/de/log/access.log"
ERROR_LOGFILE="/chemin/vers/un/fichier/de/log/error.log"

Maintenant, créons le fichier /etc/systemd/system/gunicorn@.service ou /etc/systemd/user/gunicorn@.service si l'on souhaite pouvoir lancer gunicorn avec un utilisateur :

[Unit]
Description=Gunicorn for project %I

[Service]
EnvironmentFile=/etc/conf.d/gunicorn_%i
WorkingDirectory=$ENV_PATH/bin
ExecStart=/bin/sh -c '"$ENV_PATH/bin/gunicorn" --bind="127.0.0.1:$PORT" --access-logformat="%({X-Real-IP}i)s %(l)s %(u)s %(t)s \'%(r)s\' %(s)s %(b)s \'%(f)s\' \'%(a)s\'" --access-logfile="$ACCESS_LOGFILE" --error-logfile="$ERROR_LOGFILE" --log-level="warning" --chdir="$SRC_PATH" "$APP_NAME.wsgi:application"'

[Install]
WantedBy=multi-user.target

Nous pouvons désormais gérer Gunicorn pour chaque projet :

systemctl start gunicorn@monprojet
systemctl stop gunicorn@monprojet
systemctl enable gunicorn@monprojet
systemctl disable gunicorn@monprojet

Pensez à ajouter l'option --user si vous désirez le lancer en mode utilisateur. Notez que lancer gunicorn avec un utilisateur ne nécessite bien entendu pas les accès root. C'est très pratique dans le cadre d'un déploiement automatisé, par exemple avec git et un post-receive hook.

Liens utiles