Gunicorn and company

Configuración server Django con Nginx, Gunicorn y postgreSQL

Se recomienda antes haber realizado la configuración inicial para un servidor antes de abordar esta guía.

Objetivos

Se pretende preparar un servidor para el despliegue de aplicaciones django. Para ello:

  • instalaremos nginx, gunicorn, pip y virtualenv,
  • prepararemos la base de datos,
  • instalaremos las dependencias del proyecto django,
  • configuraremos gunicorn
  • y configuraremos nginx

Cuando veas un bloque de comandos debes saber que utilizaré para referirme a una salida, la respuesta que nos dará el terminal.

Para facilitar la ejecución de las instrucciones, se han declarado a modo de constantes una serie de palabras reservadas para que el lector pueda realizar una búsqueda masiva y pueda reemplazar todas las cadenas encontradas, adaptando de esta manera los comandos al caso de uso del lector.

  • <server_ip>: Dirección IP del servidor que alojará nuestra web.
  • <user>: Usuario que tendrá acceso mediante SSH al servidor el cual pertenecerá al grupo sudo.
  • <git_url_project>: Url del repositorio Django a desplegar.
  • <project_name>: Nombre de la aplicación Django a desplegar descargado del repositorio git.
  • <db_schema>: Nombre de la base de datos que será utilizada por nuestra aplicación Django.
  • <db_user>: Usuario de la base de datos.
  • <db_password>: Password para el usuario de nuestra base de datos.
  • <betatester>: Nombre para el usuario con acceso a pre (Este paso es opcional, depende de si deseas realizar: Sistema de acceso mediante usuario+password).

Instalación de dependencias

Como primer paso, actualizaremos la paquetería e instalaremos las dependencias:

sudo apt update
sudo apt upgrade

sudo apt install gcc python3-pip python3.7-dev libpq-dev postgresql postgresql-contrib nginx git
sudo -H pip3 install --upgrade pip
sudo -H pip3 install virtualenv

Preparación del entorno virtual e instalación de dependencias

Usaremos virtualenv y pip para aislar las aplicación Django.

Asumimos que el proyecto a desplegar está publicado en un repositorio online.

cd ~
git clone <git_url_project>
cd <project_name>
virtualenv --python=/usr/bin/python3.7 <project_name>env
pip install django gunicorn psycopg2
pip install -r requirements.txt

Se espera que exista el fichero requirements.txt con todas las dependencias del proyecto.

Preparación de la base de datos

Entramos en la consola de postgres:

sudo -u postgres psql

Creamos el esquema, el usuario y configuramos ciertos parámetros:

# consola de postgres
CREATE DATABASE <db_schema>;
CREATE USER <db_user> WITH PASSWORD '<db_password>';
ALTER ROLE <db_user> SET client_encoding TO 'utf8';
ALTER ROLE <db_user> SET default_transaction_isolation TO 'read committed';
ALTER ROLE <db_user> SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE <db_schema> TO <db_user>;
\q

Preparación del proyecto

Debemos asegurarnos que en nuestro fichero settings.py:

  • La ip de nuestro servidor (<server_ip>), o dominio, esté definida en ALLOWED_HOSTS.
  • La configuración de nuestra base de datos esté correctamente plasmada en DATABASES.
  • Existe la constante STATIC_ROOT y tiene definida la ruta para los estáticos.

Aplicamos las migraciones:

# ~/<project_name>
python manage.py migrate

Generamos los estáticos:

# ~/<project_name>
python manage.py collectstatic

Generamos los .mo de los idiomas:

# ~/<project_name>
django-admin compilemessages --locale=es

Creación del servicio de Gunicorn

Creamos el fichero /etc/systemd/system/gunicorn.service y le añadimos el siguiente contenido:

# nano
[Unit]
Description=gunicorn daemon
After=network.target

[Service]
User=<user>
Group=www-data
WorkingDirectory=/home/<user>/<project_name>
ExecStart=/home/<user>/<project_name>/<project_name>env/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/<user>/<project_name>/<project_name>.sock main.wsgi:application

[Install]
WantedBy=multi-user.target

Iniciamos el servicio:

sudo systemctl start gunicorn
sudo systemctl enable gunicorn

Para ver el estado de gnunicorn

sudo systemctl status gunicorn

Tip: Con sockets puedes ganar sobre el 5% de velocidad en las respuestas pero a cambio dificulta mucho la traza de los errores, así que si lo prefieres puedes ver de definir el servicio a través de http sin sockets. ExecStart=/home/<user>/<project_name>/<project_name>env/bin/gunicorn --access-logfile - --workers 3 --bind 0.0.0.0:8000 main.wsgi:application

Creación del fichero de configuración de Nginx

Creamos el fichero /etc/nginx/sites-available/gunicorn con el siguiente contenido:

# nano
server {
    listen 80;
    server_name <server_ip>; 

    location = /static/favicon.svg { 
        root /home/<user>/<project_name>;
    }

    location /static/ {
        root /home/<user>/<project_name>;
    }

    location / {
        proxy_pass http://unix:/home/<user>/<project_name>/<project_name>.sock;
        include proxy_params;
    }
}

Tip: Si el servicio de Gunicorn lo declaramos sin el uso de sockets, será necesario cambiar el proxy pass por: proxy_pass http://127.0.0.1:8000

Crearemos un soft link en sites-enabled:

sudo ln -s /etc/nginx/sites-available/gunicorn /etc/nginx/sites-enabled

Y finalmente verificamos que la sintaxis es correcta y reiniciamos nginx:

sudo nginx -t 
→ nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
→ nginx: configuration file /etc/nginx/nginx.conf test is successful

sudo systemctl restart nginx

En caso de tener restriciones de firewall, abrimos acceso

Si realizamos la guía configuración inicial para un servidor, tendremos un firewall que solo da acceso a ssh, por lo tanto, deberemos habilitar que Nginx sea visible.

Añadimos la excepción al firewall:

sudo ufw allow 'Nginx Full'

[EXTRA] Comandos útiles para tracear un poco

Log de errores de Nginx:

sudo tail -F /var/log/nginx/error.log

Log de accesos de Nginx:

sudo tail -F /var/log/nginx/access.log

Logs de Gunicorn:

sudo journalctl -u gunicorn

Reiniciar servicio:

sudo systemctl daemon-reload
sudo systemctl restart gunicorn

Requerido al modificar el fichero del servicio

[EXTRA] Sistema de acceso mediante usuario+password

Imaginemos que queremos tener dos entornos, preproducción y producción, siendo uno de ellos el entorno beta para realizar pruebas por un grupo de testers. Una de las soluciones que se nos presenta es limitar el acceso a pre mediante usuario-contraseña, para lo cual necesitaremos hacer una pequeña modificación a nuestro fichero de configuración en Nginx.

Creamos un fichero de usuario+password, para ello ejecutaremos:

sudo sh -c "echo -n '<betatester>:' >> /etc/nginx/.htpasswd"
sudo sh -c "openssl passwd -apr1 >> /etc/nginx/.htpasswd"
→ Password:
cat /etc/nginx/.htpasswd
<betatester>:$apr1$szR.y8ij$llavOdo1KgebphhA1vVkj0

Editamos el fichero de configuración de Nginx y añadimos a la sección location / la autentificación, quedando de la siguiente forma el contenido del fichero:

# nano
server {
    listen 80;
    server_name <server_ip>;
    auth_basic "Restricted Content";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location = /static/favicon.svg { 
        root /home/<user>/<project_name>;
    }

    location /static/ {
        root /home/<user>/<project_name>;
    }

    location / {
        proxy_pass http://unix:/home/<user>/<project_name>/<project_name>.sock;
        include proxy_params;
    }
}

Verificamos que la sintaxis es correcta y reiniciamos Nginx:

sudo nginx -t 
→ nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
→ nginx: configuration file /etc/nginx/nginx.conf test is successful

sudo systemctl restart nginx

[EXTRA] Replicar producción en local

Personalmente me gusta replicar al máximo posible el entorno de producción en local, por lo tanto, si has realizado la guía configuración inicial para un servidor dispondrás de un usuario en producción creado, ese usuario yo lo crearía en local sin el grupo sudo. En caso de que no hayas realizado dicha guía, puedes saltarte el paso de crear un usuario.

Creamos el usuario para Gunicorn y deshabilitamos el password para que no nos salga en el login del sistema operativo el usuario:

adduser <user>
sudo passwd -d <user>

A continuación, seguiríamos todos los pasos anteriores descritos en esta guía, salvo En caso de tener restricciones de firewall, abrimos acceso y [EXTRA] Sistema de acceso mediante usuario+password.

Y finalmente, para aislar el servicio publicado por nuestro nginx, realizaremos una pequeña modificación al site, para ello editaremos /etc/nginx/sites-available/gunicorn añadiendo allow y deny all al bloque a server y cambiaremos el server_name a localhost, quedando de la siguiente manera el fichero de configuración:

# nano
server {
    listen 80;
    server_name 127.0.0.1; 

    allow 127.0.0.1;
    allow 192.168.1.0/16;
    deny all;

    location = /static/favicon.svg { 
        root /home/<user>/<project_name>;
    }

    location /static/ {
        root /home/<user>/<project_name>;
    }

    location / {
        proxy_pass http://unix:/home/<user>/<project_name>/<project_name>.sock;
        include proxy_params;
    }
}

Al haber definido las excepciones para localhost y para la 192.168.1.0/16 conseguimos que no se pueda acceder al servidor desde fuera de nuestra LAN, ya que si no se trabaja una de estas IPs se aplicará el deny all por defecto a cualquier otra máquina que intente acceder.

Referencias externas

  • Guía para levantar un Django, con Gunicorn+Nginx, de DigitalOcean
  • Guía para establecer autentificación en Nginx, de DigitalOcean