Django, Gunicorn, Nginx y Supervisor

Django: Under The Hood Day 2 of Django: Under The Hood 2015 por Bartek Pawlik. CC BY. Imagen con calidad y tamaño reducidos.

En este artículo vamos a explicar como desplegar una aplicación web Django utilizando Gunicorn, Nginx y Supervisor. En un artículo anterior explicamos cómo crear una aplicación web con Django, desarrollando una sencilla aplicación web de lista de tareas desde cero. En esta oportunidad, vamos desplegar dicha aplicación en un servidor web.

La arquitectura que planeamos implementar se encuentra resumida en el siguiente diagrama:

Arquitectura Django + Gunicorn + Nginx + Supervisor

Podemos observar que Nginx es inicialmente el encargado de resolver la petición web realizada por el cliente, re-enviandola para que Gunicorn pueda procesarla utilizando varios workers. Cada worker es capaz de comunicarse con la aplicación Django de manera que pueden atenderse multiples peticiones al mismo tiempo, manteniendo un buen tiempo de respuesta.

Por otro lado, Supervisor se encargará de controlar la ejecución del proceso Gunicorn de tal forma que podamos iniciar, detener o reiniciar la aplicación de Django en el momento que sea necesario.

Instalación de la aplicación web Django en el servidor

Copiando archivos con Rsync

Si tenemos los archivos de la aplicación en nuestra computadora, podemos copiarlos al servidor utilizando Rsync. Rsync es una utilidad de código abierto que permite realizar transferencias incrementales de archivos de forma rápida.

Una de las ventajas de utilizar Rsync es que permite acelerar la transferencia de archivos, subiendo unicamente las modificaciones realizadas a los archivos y los archivos que no existan en el servidor.

Si Rsync no esta instalado en nuesta computadora o servidor, podemos instalarlo usando el siguiente comando:

# macOS (con Homebrew)
$ brew install rsync

# Debian o Ubuntu
$ sudo apt-get install rsync

# Fedora
$ sudo dnf install rsync

# CentOS
$ sudo yum install rsync

# Archlinux
$ sudo pacman -S rsync

Rsync debe estar instalado tanto en nuestra máquina como en el servidor.

Para copiar los archivos al servidor, ejecutamos el siguiente comando:

$ rsync -avz <local_dir> <user>@<ip_domain>:<remote_dir>

El comando recibe los siguientes parametros:

  • -avz: Estas son opciones que permiten mantener los enlaces y los permisos de los archivos, además de comprimirlos para hacer que la transferencia se realice lo más rápido posible.
  • <local_dir>: Es el directorio en nuestra computadora, donde se encuentran alojados los archivos que queremos copiar al servidor.
  • <user>@<ip_domain>: El usuario y el nombre de dominio o IP del servidor web.
  • <remote_dir>: Es el directorio en el servidor, donde se van a copiar los archivos.

Por ejemplo, si el proyecto se encuentra en el directorio home de nuestra máquina, y vamos a copiar los archivos al directorio /home/debian/todo-django/ en el servidor, el comando a ejecutar será el siguiente:

$ rsync -avz ~/todo-django/ debian@remote:/home/debian/todo-django/

En algunas situaciones es necesario cambiar el puerto de conexión o agregar una llave de acceso a la transferencia. Como Rsync utiliza SSH para realizar la conexión al servidor, podemos especificar las opciones para SSH de la siguiente forma:

$ rsync -avz -e 'ssh -p <port> -i <key>' <local_dir> <user>@<ip_domain>:<remote_dir>

Clonando la aplicación con Git

Otra forma de copiar los archivos de la aplicación al servidor es clonando el repositorio de código fuente. En este caso la aplicación está alojada en un repositorio Git en Github, así que para poder copiarla necesitamos instalar Git en el servidor:

# Debian o Ubuntu
$ sudo apt-get install git

# CentOS
$ sudo yum install git

# Archlinux
$ sudo pacman -S git

A continuación clonamos el repositorio usando el comando git en el servidor, de la siguiente forma:

$ git clone https://github.com/rukbotto/todo-django.git /home/<user>/todo-django

En este caso, estamos copiando los archivos al directorio /home/<user> en el servidor.

Configuración de la aplicación web Django

En primer lugar necesitamos instalar Django y PostgreSQL. Para instalar Django podemos seguir las instrucciones sobre cómo instalar Django. Adicionalmente, el artículo cómo instalar y configurar PostgreSQL explica en detalle el proceso de instalación de PostgreSQL.

Con Django y PostgreSQL ya instalados, el siguiente paso consiste en crear la base de datos y el usuario que la va a administrar. Para saber que nombre debe tener el usuario y la base de datos, se debe revisar la variable DATABASES en el archivo settings.py de la aplicación web Django:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'todo_django_db',
        'USER': 'todo_django_user',
        'HOST': 'localhost',
        'PASSWORD': 'password'
    }
}

En este caso, la base de datos debe llamarse todo_django_db y el usuario todo_django_user con contraseña password. Con esta información a la mano, ejecutamos los siguientes comandos:

$ sudo -u postgres createuser -P -d todo_django_user
Enter password for new role:
Enter it again:

$ createdb todo_django_db -U todo_django_user -h localhost
Password:

Generación de la base de datos

Para generar la base de datos, ejecutamos el comando migrate de Django dentro del directorio de la aplicación:

(todo_django)$ cd /home/<user>/todo-django/
(todo_django)$ python manage.py migrate

Instalación y configuración de Gunicorn en el servidor

Gunicorn es un servidor HTTP WSGI para sistemas UNIX compatible con diferentes frameworks para desarrollo web, entre ellos Django, ligero en recursos de computación y bastante rápido.

Instalando Gunicorn

Si hemos instalado Django siguiendo las instrucciones del tutorial Python Django, ya debemos tener instalada la utilidad virtualenv en nuestra máquina; y así mísmo, ya debemos tener activado el entorno virtual todo_django. En ese caso, el instalador de paquetes Python pip ya está disponible y podemos usarlo de inmediato para instalar Gunicorn:

(todo_django)$ pip install gunicorn

Configurando Gunicorn

Es necesario indicarle a Gunicorn el host en donde la aplicación web Django va a estar disponible y la cantidad de procesos paralelos o workers que van a procesar las peticiones de los usuarios de la aplicación.

Para definir la configuración de Gunicorn, creamos el archivo gunicorn.conf.py de la siguiente forma:

/home/
    <user>/
        conf/
            gunicorn.conf.py

Y agregamos el siguiente fragmento de código:

import multiprocessing

bind = '127.0.0.1:8000'
workers = multiprocessing.cpu_count() * 2

Se podría pensar que entre más workers existan, más trafico puede soportar nuestra aplicación. Sin embargo, además del trafico, la cantidad de workers a definir depende también de las caracteristicas del servidor, como cantidad de procesadores o núcleos y cantidad de memoria RAM.

Como referencia, los desarrolladores de Gunicorn recomiendan que el número de workers esté definido por un número entre dos y cuatro, multiplicado por el número de núcleos que posea el servidor. Es un parametro que debe ajustarse constantemente de acuerdo a las necesidades de la aplicación.

Instalación y configuración de Nginx en el servidor

Nginx es un servidor HTTP y servidor proxy desarollado originalmente por Igor Sysoev y utilizado inicialmente en sitios rusos de alto tráfico como Yandex, el famoso buscador ruso. En la actualidad, Nginx es utilizado en el 29% de los sitios con más tráfico en el mundo.

Instalación en Debian y Ubuntu

Para instalar la versión oficial, agregamos las siguientes líneas al archivo /etc/apt/sources.list:

# Debian
deb http://nginx.org/packages/debian/ <version> nginx
deb-src http://nginx.org/packages/debian/ <version> nginx

# Ubuntu
deb http://nginx.org/packages/ubuntu/ <version> nginx
deb-src http://nginx.org/packages/ubuntu/ <version> nginx

Para modificar el archivo, se puede utilizar un editor de texto como vi, vim o nano. También se puede usar directamente la línea de comandos de la siguiente forma:

# Debian
$ sudo su -c 'echo -e "deb http://nginx.org/packages/debian/ <version> nginx\ndeb-src http://nginx.org/packages/debian/ <version> nginx" >> /etc/apt/sources.list'

# Ubuntu
$ sudo su -c 'echo -e "deb http://nginx.org/packages/ubuntu/ <version> nginx\ndeb-src http://nginx.org/packages/ubuntu/ <version> nginx" >> /etc/apt/sources.list'

<version> es el nombre de la versión del sistema operativo. Por ejemplo, stretch para Debian o xenial para Ubuntu.

Para prevenir que aparezcan advertencias de autenticación a la hora de instalar nginx, debemos descargar la llave GPG desde la página de Nginx y agregarla a apt de la siguiente forma:

$ wget -qO - https://nginx.org/keys/nginx_signing.key | sudo apt-key add -

De esta forma la validez del repositorio puede ser verificada correctamente.

Para finalizar ejecutamos:

$ sudo apt-get update
$ sudo apt-get install nginx

Instalación en CentOS

Para instalar la versión oficial de Nginx, creamos el archivo /etc/yum.repos.d/nginx.repo y agregamos las siguientes líneas:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/<version>/$basearch/
gpgcheck=0
enabled=1

Para crear el archivo podemos utilizar un editor de texto como vi, vim o nano. También podemos usar la línea de comandos de la siguiente forma:

$ sudo su -c 'echo -e "[nginx]\nname=nginx repo\nbaseurl=http://nginx.org/packages/centos/<version>/\$basearch/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/nginx.repo'

<version> es el número de la versión del sistema operativo. Por ejemplo, 7 para la versíon 7 de CentOS.

Para finalizar ejecutamos:

$ sudo yum update
$ sudo yum install nginx

Configurando Nginx

El siguiente fragmento de código describe la configuración completa de Nginx que vamos a usar para servir nuestra aplicación Django:

worker_processes 1;

user nobody nogroup;
# 'user nobody nobody;' for systems with 'nobody' as a group instead
pid /tmp/nginx.pid;
error_log /home/<user>/log/nginx.error.log;

events {
  worker_connections 1024; # increase if you have lots of clients
  accept_mutex off; # set to 'on' if nginx worker_processes > 1
  # 'use epoll;' to enable for Linux 2.6+
  # 'use kqueue;' to enable for FreeBSD, OSX
}

http {
  include /etc/nginx/mime.types;
  # fallback in case we can't determine a type
  default_type application/octet-stream;
  access_log /home/<user>/log/nginx.access.log combined;
  sendfile on;

  upstream app_server {
    # fail_timeout=0 means we always retry an upstream even if it failed
    # to return a good HTTP response

    # for UNIX domain socket setups
    # server unix:/tmp/gunicorn.sock fail_timeout=0;

    # for a TCP configuration
    server 127.0.0.1:8000 fail_timeout=0;
  }

  server {
    # if no Host match, close the connection to prevent host spoofing
    listen 80 default_server;
    return 444;
  }

  server {
    # use 'listen 80 deferred;' for Linux
    # use 'listen 80 accept_filter=httpready;' for FreeBSD
    listen 80;
    client_max_body_size 4G;

    # set the correct host(s) for your site
    server_name todo-django.local;

    keepalive_timeout 5;

    # path for static files
    root /home/<user>/static;

    location / {
      # checks for static file, if not found proxy to app
      try_files $uri @proxy_to_app;
    }

    location /static {
      # path for Django static files
      alias /home/<user>/static;
    }

    location @proxy_to_app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      # enable this if and only if you use HTTPS
      # proxy_set_header X-Forwarded-Proto https;
      proxy_set_header Host $http_host;
      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;
      proxy_pass http://app_server;
    }

    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /home/<user>/static;
    }
  }
}

<user> es el nombre de usuario que actualmente administra el sistema.

Guardamos el fragmento de código anterior en el archivo nginx.conf de la siguiente forma:

/home/
    <user>/
        conf/
            nginx.conf

El fragmento anterior está basado en el ejemplo de configuración de Nginx facilitado por los desarrolladores de Gunicorn.

En sistemas CentOS, debido a que utilizan SELinux para mejorar la seguridad del sistema, el usuario y el grupo no puede ser definido como user nobody nogroup, ya que SELinux no permitirá que Nginx acceda a ninguno de los archivos de la aplicación.

Para solucionar este problema, se le debe indicar a Nginx que se ejecute usando el usuario y el grupo que actualmente administra el sistema. Por ejemplo, si el usuario del servidor se denomina centos y el grupo al que pertenece también se denomina centos, la tercera línea en el archivo de configuración de Nginx deberá ser user centos centos;.

En la sección upstream, le indicamos a Nginx el socket o el host donde está disponible la aplicación Django. Como recordarán, en la configuración de Gunicorn definimos que la aplicación va a estar disponible en http://127.0.0.1:8000:

upstream app_server {
  # fail_timeout=0 means we always retry an upstream even if it failed
  # to return a good HTTP response

  # for UNIX domain socket setups
  # server unix:/tmp/gunicorn.sock fail_timeout=0;

  # for a TCP configuration
  server 127.0.0.1:8000 fail_timeout=0;
}

La primera sección server le indica a Nginx que cierre la conexión inmediatamente si el usuario utiliza la dirección IP del servidor para cargar el sitio web en el navegador. Esta es una medida de seguridad contra ataques del tipo Host Spoofing:

server {
  # if no Host match, close the connection to prevent host spoofing
  listen 80 default_server;
  return 444;
}

En la siguiente sección server le indicamos a Nginx el nombre de dominio mediante el cual vamos a poder acceder a nuestra aplicación desde el exterior:

# set the correct host(s) for your site
server_name todo-django.local;

En las subsecciones location que aparecen enseguida, podemos apreciar la configuración para que Nginx redireccione las peticiones al proceso Gunicorn:

location / {
  # checks for static file, if not found proxy to app
  try_files $uri @proxy_to_app;
}

location @proxy_to_app {
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  # enable this if and only if you use HTTPS
  # proxy_set_header X-Forwarded-Proto https;
  proxy_set_header Host $http_host;
  # we don't want nginx trying to do something clever with
  # redirects, we set the Host: header above already.
  proxy_redirect off;
  proxy_pass http://app_server;
}

Cuando un usuario solicite la URL /, Nginx va a redirigir la petición a la sección location @proxy_to_app. Esta última, a su vez, va a enviar la petición a la sección upstream que explicamos anteriormente, es decir a nuestra aplicación Django.

A continuación, creamos los directorios log y static para alojar los logs de acceso y error y los archivos estáticos de la aplicación respectivamente:

/home/
    <user>/
      log/
      static/

Configurando el nombre de dominio

Para que Django permita cargar la aplicación correctamente, debemos verificar que la variable ALLOWED_HOSTS en el archivo settings.py de la aplicación, contenga el nombre de dominio que definimos en el archivo de configuración de Nginx:

ALLOWED_HOSTS = ['todo-django.local']

Además, es necesario agregar el nombre de dominio al archivo hosts de nuestra máquina así:

# Si la aplicación web Django se encuentra en nuestra máquina
127.0.0.1       localhost todo-django.local

# Si la aplicación web Django se encuentra en una máquina externa o una VM
<server_ip_dir> todo-django.local

Para sistemas Linux y macOS, el archivo hosts se encuentra en el directorio /etc. En sistemas Windows 10, el archivo se encuentra en el directorio C:\Windows\System32\drivers\etc.

Instalación y configuración de Supervisor en el servidor

Supervisor es un sistema cliente/servidor que permite monitorear y controlar procesos en sistemas UNIX. Fué diseñado para controlar procesos relacionados a un proyecto.

Instalando Supervisor

Para instalar Supervisor utilizamos el instalador pip para Python 2:

$ sudo pip install supervisor

Es necesario desactivar cualquier entorno virtual activo mediante el comando deactivate para realizar esta operación.

Si pip para Python 2 no está instalado, podemos instalarlo siguiendo las instrucciones sobre cómo instalar un paquete Python con pip.

Se hace necesario instalar Supervisor de forma global en el sistema utilizando Python 2, porque actualmente no es compatible con Python 3. Por esta razón, no es posible instalarlo en el mismo entorno virtual donde se ha instalado la aplicación web Django.

Configurando Supervisor

Con Supervisor vamos a poder iniciar, detener o reiniciar la ejecución del proceso Gunicorn, es decir, la aplicación web Django. Para que esto sea posible, debemos indicarle a Supervisor todos los detalles necesarios acerca del proceso Gunicorn.

Creamos el archivo supervisord.conf de la siguiente manera:

/home/
    <user>/
        conf/
            supervisord.conf

Y agregamos las siguientes líneas:

[supervisord]
logfile=/home/<user>/log/supervisord.log

[inet_http_server]
port=127.0.0.1:9001

[rpcinterface:supervisor]
supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface

[program:todo-django]
command=/home/<user>/.virtualenvs/todo_django/bin/gunicorn todo_django.wsgi -c /home/<user>/conf/gunicorn.conf.py
directory=/home/<user>/todo-django
user=nobody
autostart=true
autorestart=true
stdout_logfile=/home/<user>/log/todo-django.log
stderr_logfile=/home/<user>/log/todo-django.err.log

<user> es el nombre de usuario que actualmente administra el sistema.

Las secciones [supervisord], [inet_http_server] y [rpcinterface:supervisor] son obligatorias. La sección [inet_http_server] le indica a Gunicorn cual es el puerto en el que debe iniciar el servidor HTTP.

En la sección [program:todo-django] definimos un programa llamado todo-django. Especificamos el comando, que en este caso es el ejecutable de Gunicorn, el cual está alojado en el entorno virtual todo_django.

La variable directory permite definir el directorio desde dónde Gunicorn debe ejecutarse. Supervisor “entrará” a este directorio antes de ejecutar el comando definido.

En sistemas CentOS, de la misma manera que sucede con Nginx, el usuario que debe ejecutar el programa todo-django debe ser el que actualmente administra el sistema. Si el usuario del servidor es centos, entonces el usuario para el programa debe definirse así: user=centos.

Por último, especificamos la ubicación de los archivos log para mensajes estandar y mensajes de error.

Controlando la ejecución de la aplicación Django

Teniendo los archivos de configuración debidamente creados, ahora podemos controlar la ejecución de la aplicación web Django interactuando solamente con Nginx y Supervisor.

Cómo iniciar, detener o reiniciar Nginx

Podemos iniciar Nginx de la siguiente forma:

$ sudo nginx -c /home/<user>/conf/nginx.conf

Si cargamos la dirección http://todo-django.local en nuestro navegador, vamos a comprobar que Nginx arroja un error 404 Not Found. Esto se debe a que Gunicorn aún no ha sido ejecutado:

Nginx página no encontrada

Para detener Nginx, podemos ejecutar el siguiente comando:

$ sudo pkill -QUIT nginx

Y para reiniciarlo:

$ sudo pkill -HUP nginx

Cómo iniciar, detener o reiniciar Supervisor

Lo primero es iniciar el demonio de Supervisor supervisord, de la siguiente forma:

$ sudo supervisord -c /home/<user>/conf/supervisord.conf

Para detener el demonio de Supervisor, podemos ejecutar el siguiente comando:

$ sudo pkill -QUIT supervisord

Y para reiniciarlo:

$ sudo pkill -HUP supervisord

Con el demonio corriendo en background, utilizamos la utilidad supervisorctl para iniciar, detener o reiniciar la aplicación web Django:

# Para iniciar la aplicación
$ supervisorctl start todo-django

# Para detener la aplicación
$ supervisorctl stop todo-django

# Para reiniciar la aplicación
$ supervisorctl restart todo-django

todo-django es el nombre del programa que se definió en el archivo de configuración de Supervisor: [program:todo-django].

Recolectando los archivos estáticos

Con Gunicorn y Nginx corriendo en background, ya podemos cargar la aplicación en nuestro navegador. Sin embargo, al abrir la página de administración de Django en la dirección http://todo-django.local/admin, podemos ver que los archivos CSS y Javascript no se han podido cargar:

Página de administración de Django sin estilos

Para solucionar este problema, necesitamos recolectar los archivos estáticos usando el comando collectstatic de Django. Para esto, primero debemos verificar si la variable STATIC_ROOT se encuentra definida en el archivo settings.py de la aplicación web Django:

STATIC_ROOT = os.path.join(BASE_PATH, '../static')

La variable STATIC_ROOT le indica a Django el directorio en el cual debe copiar los archivos estáticos de todas las aplicaciones implementadas en el proyecto. De esta forma es posible servir los archivos desde allí mismo o copiar el directorio a otro servidor o a una CDN.

En nuestro caso, le indicamos a Django que debe copiar todos los archivos estáticos en el directorio ../static relativo a la ubicación del archivo manage.py, es decir, en el directorio /home/<user>/static.

Posteriormente ejecutamos el comando collectstatic dentro del directorio de la aplicación, sin olvidar reactivar el entorno virtual todo_django:

$ source /home/<user>/.virtualenvs/todo_django/bin/activate
(todo_django)$ cd /home/<user>/todo-django
(todo_django)$ python manage.py collectstatic

Si volvemos a acceder a la página de administración, comprobaremos que los archivos estáticos cargan correctamente:

Página de administración de Django con estilos

Probando la aplicación

En nuestro navegador favorito, cargamos la dirección http://todo-django.local y debemos ver la aplicación de lista de tareas:

Aplicación web Django lista de tareas

Podemos interactuar con la aplicación para comprobar que todo esté funcionando normalmente.

Es así como llegamos al final de este artículo. ¡Hasta una próxima oportunidad!