Deploy Django app con 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 hacer el deploy de una aplicación Django en un servidor usando Nginx, Gunicorn 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.

Instalando la aplicación en el servidor

En esta sección se explicará como copiar los archivos de la aplicación al servidor web utilizando Rsync y Git. A continuación, se demostrará cómo instalar Django y PostgreSQL. Por último, se mostrará cómo generar la base de datos.

Copiando archivos con Rsync

Si ya 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:

>> Archlinux
$ sudo pacman -S rsync

>> Debian, Ubuntu
$ sudo apt-get install rsync

>> Fedora
$ sudo dnf install rsync

>> CentOS
$ sudo yum install rsync

>> macOS (con Homebrew)
$ brew install rsync

Para que Rsync funcione apropiadamente, debe estar instalado tanto en nuestra máquina como en el servidor.

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

$ rsync -avz todo-django/ <user>@<ip_domain>:/server/dir/todo-django/

El comando tiene las siguientes partes:

  • -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.
  • todo_django/: Es el directorio, en nuestra computadora, donde se encuentra alojado el código fuente de la aplicación.
  • <user>@<ip_domain>: El usuario y el dominio o IP del servidor web.
  • /server/dir/todo-django/: Es el directorio, en el servidor, donde se van a copiar los archivos de la aplicación.

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>' todo-django/ <user>@<ip_domain>:/server/dir/todo-django/

En el ejemplo anterior, le estamos indicando a SSH que se conecte por un puerto diferente al puerto estandar.

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, Ubuntu
$ sudo apt-get install git

>> CentOS
$ sudo yum install 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 /server/dir/todo-django

Instalando Django y PostgreSQL

Para instalar Django y PostgreSQL se pueden seguir las instrucciones de la sección Instalando Django y PostgreSQL del Tutorial de Python Django.

Ahora necesitamos crear la base de datos y el usuario que la va a administrar. Para saber como se debe nombrar el usuario y la base de datos, se debe revisar la variable DATABASES en el archivo settings.py de la aplicación:

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:

Generando la base de datos

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

$ cd /server/dir/todo-django/
$ python manage.py migrate

Los usuarios Archlinux deben ejecutar python2 en lugar de python.

Instalando Nginx en el servidor

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

Esta sección explica como instalar Nginx en sistemas Debian, Ubuntu y CentOS.

Debian y Ubuntu

Las siguientes instrucciones aplican para la versión 16.04 de Ubuntu y la versión 8 de Debian.

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

# Debian
deb http://nginx.org/packages/debian/ jessie nginx
deb-src http://nginx.org/packages/debian/ jessie nginx

# Ubuntu
deb http://nginx.org/packages/ubuntu/ xenial nginx
deb-src http://nginx.org/packages/ubuntu/ xenial 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/ jessie nginx\ndeb-src http://nginx.org/packages/debian/ jessie nginx" >> /etc/apt/sources.list'
[sudo] password for <user>:

>> Ubuntu
$ sudo su -c 'echo -e "deb http://nginx.org/packages/ubuntu/ xenial nginx\ndeb-src http://nginx.org/packages/ubuntu/ xenial nginx" >> /etc/apt/sources.list'
[sudo] password for <user>:

El comando sudo su nos pedirá la contraseña del usuario del servidor para poder modificar el archivo exitosamente.

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

CentOS

Las siguientes instrucciones aplican para la versión 7 de 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/7/$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/7/\$basearch/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/nginx.repo'
[sudo] password for <user>:

El comando sudo su nos pedirá la contraseña del usuario del servidor para poder crear el archivo exitosamente.

Para finalizar ejecutamos:

$ sudo yum update
$ sudo yum install nginx

Instalando Gunicorn y Supervisor en el servidor

Gunicorn es un servidor HTTP WSGI para sistemas UNIX compatible con Django. Por otro lado, Supervisor es un sistema cliente/servidor que permite monitorear y controlar procesos en sistemas UNIX.

Para instalar Gunicorn y Supervisor vamos a necesitar del instalador de paquetes de Python llamado pip. Para verificar si nuestro servidor tiene pip instalado, ejecutamos el siguiente comando:

$ pip -V

Si al ejecutar el comando anterior aparece el siguiente mensaje:

-bash: pip: command not found

Significa que pip no está instalado en el servidor. Para instalarlo, se pueden seguir las instrucciones de la sección Cómo instalar pip usando una distribución Linux del artículo Cómo instalar un paquete Python con pip.

Con pip ya instalado, ejecutamos el siguiente comando para instalar las librerías mencionadas:

$ sudo pip install gunicorn supervisor

Los usuarios Archlinux deben ejecutar pip2 en lugar de pip, si pip fue instalado usando el gestor de paquetes.

Configurando Gunicorn

Es necesario indicarle a Gunicorn la dirección en donde nuestra aplicación 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:

/server/dir/
  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 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.

Configurando Supervisor

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

Creamos el archivo supervisord.conf:

/server/dir/
  conf/
    supervisord.conf

Y agregamos las siguientes líneas:

[supervisord]
logfile=/server/dir/log/supervisord.log

[inet_http_server]
port=127.0.0.1:9001

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

[program:gunicorn]
command=gunicorn todo_django.wsgi -c /server/dir/conf/gunicorn.conf.py
directory=/server/dir/todo-django
user=nobody
autostart=true
autorestart=true
redirect_stderr=true

Supervisor no funcionará correctamente si las secciones [supervisord] y [rpcinterface:supervisor] estan ausentes. Si la sección [inet_http_server] no está presente, Supervisor no iniciará un servidor HTTP y por lo tanto nuestra aplicación no será accesible.

En la sección [program:gunicorn] le indicamos a Supervisor que controle la ejecución del proceso Gunicorn. Especificamos el usuario y el directorio de la aplicación, que es desde donde Gunicorn debe ejecutarse. También especificamos si Gunicorn debe auto-iniciarse y auto-reiniciarse y si los errores deben redireccionarse a la salida estandar.

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 /server/dir/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 /server/dir/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 /server/dir/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/admin/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 /server/dir/static;
    }
  }
}

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

/server/dir/
  conf/
    nginx.conf

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

En la sección upstream, le indicamos a Nginx el socket o la dirección IP 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 la dirección 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 host o 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;

Para que Django permita cargar la aplicación correctamente, debemos agregar el dominio a la variable ALLOWED_HOSTS en el archivo settings.py:

ALLOWED_HOSTS = ['todo-django.local']

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

# Si el servidor se encuentra en nuestra máquina
127.0.0.1       localhost todo-django.local

# Si el servidor 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.

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:

/server/dir/
  log/
  static/

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 Django interactuando solamente con Nginx y Supervisor.

Interactuando con Nginx

Podemos iniciar Nginx de la siguiente forma:

$ sudo nginx -c /server/dir/conf/nginx.conf

Si abrimos 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:

Aplicación Django

Para detener Nginx, podemos ejecutar el siguiente comando:

$ sudo pkill -QUIT nginx

Y para reiniciar Nginx:

$ sudo pkill -HUP nginx

Interactuando con Supervisor

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

$ sudo supervisord -c /server/dir/conf/supervisord.conf

Con el demonio corriendo en background, utilizamos la utilidad supervisorctl para iniciar, detener o reiniciar Gunicorn:

>> Para iniciar Gunicorn
$ supervisorctl start gunicorn

>> Para detener Gunicorn
$ supervisorctl stop gunicorn

>> Para reiniciar Gunicorn
$ supervisorctl restart gunicorn

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, veremos que los archivos CSS y Javascript no se han podido cargar:

Django admin

Para solucionar este problema, necesitamos recolectar los archivos estáticos usando el comando collectstatic de Django. Para esto, primero debemos definir la variable STATIC_ROOT en el archivo settings.py de la siguiente forma:

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

La variable STATIC_ROOT le indica a Django el directorio donde debe recolectar 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 recolectar los archivos estáticos en el directorio ../static relativo a la ubicación del archivo manage.py, es decir, en el directorio /server/dir/static.

Posteriormente ejecutamos el comando collectstatic dentro del directorio de la aplicación:

$ cd /server/dir/todo-django
$ python manage.py collectstatic

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

Django admin

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:

Django app

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

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