Deploy de un proyecto hecho en Django en un VPS con Ubuntu 20.04, Nginx, Gunicorn y PostgreSQL
Mucha gente lo ha pedido y por fin está aquí 🎉. Después de terminar los tutoriales que quería hacer sobre Python (por el momento), es hora de retomar otras cositas que también pueden ser interesantes para vosotr@s. En el tutorial de esta semana vamos a ver paso a paso como realizar el deploy de un proyecto hecho en Django en un VPS con Ubuntu 20.04, Nginx, Gunicorn y PostgreSQL.
Antes de empezar...
Para realizar este tutorial necesitaremos dos cosas. Lo primero será un vps o un server del que tengamos un usuario con permisos de administración y el sistema operativo Ubuntu. Hay varios proveedores en los que podéis contratar un vps barato. Para este tutorial yo he contratado el vps más barato que tenía OVH que son unos 3.5 €, pero vamos, podéis contratarlo donde queráis. Lo importante es tener un usuario con permisos de administración y Ubuntu.
Lo segundo será el proyecto en un repositorio de git, ya sea GitHub, GitLab o Bitbucket. En mi caso, vamos a realizar el deploy de este proyecto que creé en un tutorial pasado en el que creamos un blog con Django desde 0.
Opcionalmente un dominio para que apunte a nuestro servidor y podamos activar https.
Paso 1: Comprobar la configuración del proyecto
En el ámbito del proyecto necesitaremos algunas cositas. Lo primero es comprobar que Django está configurado para utilizar PostgreSQL. Para ello debemos ir al archivo settings.py que está dentro de la carpeta con el nombre de nuestro proyecto. Ahí vamos a la constante DATABASES y la modificamos para indicarle a Django que base de datos vamos a utilizar:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('POSTGRESQL_NAME'),
'USER': env('POSTGRESQL_USER'),
'PASSWORD': env('POSTGRESQL_PASS'),
'HOST': env('POSTGRESQL_HOST'),
'PORT': env('POSTGRESQL_PORT'),
}
}
Además del ENGINE (que es el que indica que tipo de base de datos estamos utilizando), debemos añadir, el host, nombre de la base de datos, user, password y el puerto.
Por seguridad yo guardo esa información en un archivo .env que nunca se sube al repositorio. Por eso no aparecen esos datos en crudo en el bloque de código que tenéis más arriba.
Si queréis más información acerca de las variables de entorno y como implementarlas en un proyecto con Django, pinchad en este tutorial en el que explico como conectarse a una bbdd de PostgreSQL con Django y como utilizar las variables de entorno. También os dejo este post en Stack Overflow donde explican que son y como funcionan.
También debeis comprobad si en el archivo settings.py tenéis configuradas las constantes para los archivos estáticos y la constante ALLOWED_HOSTS. Me explico:
Para que funcione el proyecto en producción, debemos tener seteado dentro de ALLOWED_HOSTS la ip de nuestro server y/o el dominio. También valdría asterísco que significaría que permite cualquier dominio o ip:
ALLOWED_HOSTS = ['*']
# o así también valdría
ALLOWED_HOSTS = ['51.68.46.172', 'albertorcaballero.com']
Si no es así, cuando intentemos acceder desde el navegador no nos dejará.
Para poder acceder a los archivos estáticos deberemos tener configuradas las constantes donde se indican las rutas donde se almacenan estos archivos. Para verficiarlo, deberíais tener algo así en vuestro archivo settings.py. Si no es así, añadidlo:
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/admin')
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
Recordad haced estos pasos porque más adelante cuando clonemos el proyecto en el server, no vamos a poder hacer un git pull y nos tocará borrar y clonar de nuevo el proyecto.
Paso 2: Preparar nuestro servidor
Lo primero que vamos a hacer es facilitar el proceso para entrar en nuestro server. Lo normal es usar el siguiente comando y luego tendremos que añadir la contraseña:
ssh {mi-usuario}@{ip-servidor}
El proceso que vamos a realizar a continuación es totalmente opcional así que podeís pinchar aquí para saltaros esta parte.
Bien, seguís por aquí jaja, lo primero que vamos a hacer es crear una llave ssh. Para ello vamos a la terminal y lanzamos el siguiente comando:
ssh-keygen
Nos preguntará donde queremos guardarla. Lo mejor es guardarla dentro de la carpeta ~/.ssh y la podéis llamar como queráis. Yo la he llamado vps_tutorial.
El siguiente paso es añadir un passphrase que ahí ya os lo dejo a vuestra elección si queréis añadirlo. Listo, ya tenemos una llave para nuestro vps.
Generating public/private rsa key pair.
Enter file in which to save the key (/home/alber/.ssh/id_rsa): /home/alber/.ssh/vps_tutorial
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/alber/.ssh/vps_tutorial.
Your public key has been saved in /home/alber/.ssh/vps_tutorial.pub.
El siguiente paso es enviar la clave pública que acabamos de crear a nuestro server. Esto lo haremos con el siguiente comando:
ssh-copy-id -i /home/alber/.ssh/vps_tutorial.pub {mi-usuario}@{ip-servidor}
Una vez enviada la clave, debemos dirigirnos a la carpeta .ssh (en mi caso es /home/alber/.ssh) y crear un archivo al que llamaremos config (sin ninguna extensión). Dentro de él añadiremos el siguiente contenido:
ServerAliveInterval 120
ServerAliveCountMax 3
Host vps_tutorial
HostName ip-servidor
User mi-usuario
IdentityFile /home/alber/.ssh/vps_tutorial
ServerAliveInterval y ServerAliveCountMax lo utilizaremos para mantener activa la conexión. Después escribimos Host y el nombre que le queramos dar a la configuración, en mi caso lo he llamado vps_tutorial al igual que la clave ssh. En HostName indicamos la ip del servidor al que nos queremos conectar. En User el nombre del usuario con el que nos conectaremos y en IdentityFile la ruta de nuestra clave privada.
Guardamos el archivo y a partir de ahora podremos conectarnos a nuestro server simplemente lanzando el siguiente comando:
ssh {nombre_host}
# En mi caso sería así:
ssh vps_tutorial
Puede ser que la primera vez que lo utilicéis os dé problemas. Para solucionarlo, os conectáis de la forma normal (ssh {tu_usuario}@{ip_del_servidor}), salís y lo volvéis a intentar con el nuevo comando y ya deberíais entrar sin problema.
Una vez dentro del servidor, lo primero que haremos será actualizar a sus nuevas versiones los paquetes que vienen instalados por defecto en nuestro servidor:
sudo apt update && sudo apt upgrade -y
Esto es opcional, pero si queréis cambiar la hora del servidor lo podréis hacer con el siguiente comando:
sudo timedatectl set-timezone Europe/Madrid
Solo debéis sustituir Europe/Madrid por vuestra zona horaria. Os dejo la lista pinchando aquí.
Ahora vamos a configurar el Firewall que viene por defecto en Ubuntu. Para ello primero vamos a habilitar los permisos para OpenSSH que es el paquete que utilizamos para realizar el login, esto es necesario, ya que cuando habilitemos el Firewall solo podremos acceder a nuestro servidor si le hemos dado los permisos:
sudo ufw allow "OpenSSH"
Una vez hecho esto, habilitamos el Firewall:
sudo ufw enable
El siguiente paso que vamos a realizar es instalar los paquetes que necesitaremos como Python 3, Nginx y PostgreSQL:
sudo apt install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx curl
Paso 3: Configurar PostgreSQL
Ahora que ya tenemos instalado PostgreSQL, es hora de crear nuestra base de datos y el usuario que usaremos para poder trabajar con ella. Para ello, lo primero que haremos será acceder a la bbdd con el siguiente comando:
sudo -u postgres psql
Después creamos el usuario que utilizará nuestro proyecto para acceder a la bbdd:
CREATE USER {nombre-usuario} WITH PASSWORD '{contraseña}';
# Ejemplo
CREATE USER simpleblog WITH PASSWORD 'rJgtSf5fCXmdB4wH';
Os dejo esta web para que generéis contraseñas difíciles https://passwordsgenerator.net/
El siguiente paso es crear la bbdd. Yo la he llamado simpleblog y su propietario será el usuario que acabamos que crear también llamado simpleblog:
CREATE DATABASE simpleblog
WITH
OWNER = simpleblog
ENCODING = 'UTF8'
TABLESPACE = pg_default
CONNECTION LIMIT = -1;
Por último le damos a nuestro usuario todos los privilegios a la hora de trabajar con la bbdd para evitar problemas:
GRANT ALL PRIVILEGES ON DATABASE simpleblog TO simpleblog;
Una vez hecho esto, escribimos exit y pulsamos enter para salir de PostgreSQL.
Paso 4: Descargar nuestro proyecto con Git
Al instalar Nginx, este nos crea un usuario llamado www-data. Este usuario será el que se utilizará cuando accedamos a nuestra web desde el navegador, por lo tanto será el usuario que utilizaremos para clonar el proyecto desde GitHub (o desde donde tengáis el repositorio).
En mi caso, al ser un repositorio público, no haría falta tener una clave ssh, pero como vamos a seguir los pasos como si fuera un repositorio privado, vamos a crearla.
Para ello, primero creamos la carpeta .ssh dentro de la carpeta principal del usuario www-data. Esta se encuentra en la ruta /var/www. Para crearla lanzamos el siguiente comando:
sudo mkdir /var/www/.ssh
El siguiente paso es cambiar el propietario de la carpeta, ya que la hemos creado con root. Para ello lanzamos el siguiente comando:
sudo chown -R www-data /var/www/.ssh
Una vez hecho esto, vamos a crear nuestra clave pública y privada. Para ello lanzamos el siguiente comando sustituyendo email@email.com por el email con el que estéis registrado en la plataforma donde tengáis vuestro repositorio (GitHub, GitLab, etc.).
sudo -u www-data ssh-keygen -t rsa -b 4096 -C "email@email.com"
Al igual que cuando creamos la clave en nuestro PC, os debería salir algo como esto. Fijaos en que en este caso no vamos a renombrar nuestra clave, sino que mantendremos la ruta y el nombre por defecto (/var/www/.ssh/id_rsa):
Generating public/private rsa key pair.
Enter file in which to save the key (/var/www/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /var/www/.ssh/id_rsa
Your public key has been saved in /var/www/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:xxxxx email@email.com
Ahora copiamos el contenido de nuestra clave pública y la guardamos en la plataforma donde tenemos nuestro repositorio. Para ver la clave pública debemos lanzar el siguiente comando:
cat /var/www/.ssh/id_rsa.pub
Una vez hecho esto, podemos comprobar que todo ha ido bien con el siguiente comando si nuestro repositorio está en GitHub. Para otras plataformas deberéis sustituir "git@github.com" por el correspondiente a esa plataforma:
sudo -u www-data ssh -T git@github.com
Si todo ha ido bien debería aparecer el siguiente mensaje:
Hi albertorc87! You've successfully authenticated, but GitHub does not provide shell access.
Antes de descargar el proyecto debemos asegurarnos de que el usuario www-data es el propietario de la carpeta /var/www, para ello, al igual que lo hicimos con la carpeta .ssh, lanzamos el siguiente comando:
sudo chown -R www-data /var/www
Una vez hecho esto, clonamos el proyecto dentro de la carpeta /var/www. Para ello nos dirigimos a esa ruta y lanzamos el siguiente comando, en el que deberéis sustituir {repositorio} por la url de vuestro repositorio. En {nombre-carpeta} yo le voy a dar el nombre del dominio que voy a utilizar, en este caso el dominio albertorcaballero.com, que actualmente lo tengo en desuso:
sudo -u www-data git clone {repositorio} {nombre-carpeta}
# Mi caso:
sudo -u www-data git clone git@github.com:albertorc87/simple_blog.git albertorcaballero.com
Paso 5: Configuración del proyecto
Para configurar nuestro proyecto lo primero que haremos será instalar el paquete de Python para crear el entorno virtual:
sudo apt install python3-venv
Después vamos al directorio de nuestro proyecto (en mi caso /var/www/albertorcaballero.com) y lanzamos el siguiente comando para crear el entorno virtual:
sudo -u www-data python3 -m venv env
Ahora tenemos que hacer un cambio momentáneo. Para poder instalar las dependencias con el entorno virtual tenemos que estar logeados con el usuario www-data, al no poder hacer eso, vamos a cambiar por un momento el propietario de la carpeta en la que está nuestro proyecto por el usuario logueado:
sudo chown -R mi-usuario /var/www/albertorcaballero.com
Ahora activamos el entorno virtual:
source env/bin/activate
Después solo debemos instalar las dependencias de nuestro proyecto. Para ello tengo un archivo llamado requirements.txt. Si no lo tenéis y queréis crearlo para guardar vuestras dependencias, lo que debéis hacer es en el PC donde tengáis funcionado el proyecto, lanzáis el comando "pip freeze" y copiais el listado de dependencias en el archivo requirements.txt. Después solo debéis subirlo al repositorio, haced un git pull en el server y ya podréis lanzar el siguiente comando:
pip install -r requirements.txt
Aparte debemos instalar la librería de Gunicorn para Python. Esto lo haremos con el siguiente comando:
pip install gunicorn
Devolvemos la propiedad de la carpeta al usuario www-data y listo:
sudo chown -R www-data /var/www/albertorcaballero.com
Si tenéis variables de entorno como es en mi caso, debemos crear el archivo .env y rellenarlas. Para ello vamos nos dirigimos a la carpeta que se llama igual a nuestro proyecto (en mi caso /var/www/albertorcaballero.com/simple_blog) y lanzamos el siguiente comando:
sudo -u www-data nano .env
Dentro añadís vuestras variables de entorno. Yo por ejemplo tendría que añadir la autenticación a PostgreSQL y la SECRECT_KEY y listo:
# Ejemplo de mi archivo .env
SECRET_KEY=my_key
POSTGRESQL_NAME=simpleblog
POSTGRESQL_USER=simpleblog
POSTGRESQL_PASS=password
POSTGRESQL_HOST=localhost
POSTGRESQL_PORT=8754
Ahora que ya tenemos la configuración de la bbdd, es hora de crear las tablas. Para ello volvemos a la raíz del proyecto y lanzamos el siguiente comando:
python manage.py migrate
Una vez hecho esto, creamos un superusuario para poder acceder al panel de administración de Django. Esto lo haremos con el siguiente comando:
python manage.py createsuperuser
Ahora vamos a comprobar que funciona nuestro proyecto. Primero habilitamos el puerto en el firewall para poder acceder a el:
sudo ufw allow 8000
El siguiente paso es lanzar el siguiente comando en la ráiz del proyecto:
python manage.py runserver 0.0.0.0:8000
Ahora en nuestro navegador solo debemos escribir la ip del servidor seguido de dos puntos y el puerto ya deberíamos poder ver nuestro proyecto funcionando:
# En mi caso sería esta ip:
http://51.68.46.172:8000/
Paso 6: Configuración de Gunicorn
Con el proyecto ya configurado, podemos desactivar el entorno virtual con el comando deactivate:
deactivate
También podemos eliminar el permiso que creamos para el puerto 8000:
sudo ufw delete allow 8000
Ahora vamos a configurar Gunicorn. El primer paso es crear el archivo gunicorn.socket:
sudo nano /etc/systemd/system/gunicorn.socket
Añadimos la siguiente información, guardamos y cerramos:
[Unit]
Description=gunicorn socket
[Socket]
ListenStream=/run/gunicorn.sock
[Install]
WantedBy=sockets.target
Ahora creamos el archivo gunicorn.service:
sudo nano /etc/systemd/system/gunicorn.service
Y añadimos el siguiente código:
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/{mi-proyecto}
ExecStart=/var/www/{mi-proyecto}/env/bin/gunicorn \
--access-logfile - \
--workers 3 \
--bind unix:/run/gunicorn.sock \
{nombre-proyecto}.wsgi:application
[Install]
WantedBy=multi-user.target
El que debemos reemplazar {mi-proyecto} por el nombre de la carpeta donde está vuestro proyecto y {nombre-proyecto} por el nombre de vuestro proyecto. Os dejo el ejemplo de como queda el mio:
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/albertorcaballero.com
ExecStart=/var/www/albertorcaballero.com/env/bin/gunicorn \
--access-logfile - \
--workers 3 \
--bind unix:/run/gunicorn.sock \
simple_blog.wsgi:application
[Install]
WantedBy=multi-user.target
El siguiente paso es activar Gunicorn:
sudo systemctl start gunicorn.socket
sudo systemctl enable gunicorn.socket
Para comprobar que todo ha ido bien, lanzamos el siguiente comando:
sudo systemctl status gunicorn.socket
La respuesta debería ser similar a esta:
● gunicorn.socket - gunicorn socket
Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; vendor preset: enabled)
Active: active (listening) since Sun 2021-10-31 12:22:37 CET; 56s ago
Triggers: ● gunicorn.service
Listen: /run/gunicorn.sock (Stream)
Tasks: 0 (limit: 2286)
Memory: 0B
CGroup: /system.slice/gunicorn.socket
Oct 31 12:22:37 vps-685b8378 systemd[1]: Listening on gunicorn socket.
Ahora debemos confirmar que existe el archivo /run/gunicorn.sock con el siguiente comando:
file /run/gunicorn.sock
Si todo ha ido bien debería devolver esto:
/run/gunicorn.sock: socket
Si no es así o hay algún fallo, revisad los pasos anteriores para comprobar que está todo ok.
El siguiente paso es comprobar que el socket puede recibir peticiones, para ello lanzamos el siguiente comando:
curl --unix-socket /run/gunicorn.sock localhost
Esto nos devolverá un html con el index de nuestra web.
Una vez hecho esto, podemos comprobar que el servicio de Gunicorn está listo con el siguiente comando:
sudo systemctl status gunicorn
Paso 7: Configuración de Nginx
Ánimo, ya queda poco para terminar el tutorial. Ahora vamos a configurar Nginx. El primer paso será crear un archivo con la configuración de Nginx para nuestro proyecto. Para ello lanzamos el siguiente comando al que yo lo he llamado como el nombre de mi dominio. Os aconsejo que lo nombréis con el nombre de vuestro dominio porque lo necesitaremos más adelante en los certificados:
sudo nano /etc/nginx/sites-available/albertorcaballero.com
Dentro de el, debeís añadir la siguiente configuración que os explicaré a continuación:
server {
listen 80;
server_name albertorcaballero.com;
location ~ /\.ht {
deny all;
}
location ~ /\.git {
deny all;
}
location = /favicon.ico { access_log off; log_not_found off; }
# Django media
location /media {
alias /var/www/albertorcaballero.com/media/; # your Django project's media files - amend as required
}
location /static {
alias /var/www/albertorcaballero.com/static; # your Django project's static files - amend as required
}
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
}
}
server {
server_name www.albertorcaballero.com;
return 301 https://albertorcaballero.com$request_uri;
}
En server_name añado mi dominio, si aun no disponéis de uno, podéis poner la ip.
Después controlamos los accesos con location y la ruta.
En los dos primeros casos, añado un control para que no se pueda acceder al directorio .git y al .htaccess.
También hay un control para evitar los clásicos avisos de que falta el archivo /favicon.ico.
También añadimos las direcciones donde se encuentran nuestros archivos estáticos y multimedia.
Por último, apuntamos que todo el tráfico vaya a Gunicorn.
En el segundo bloque server indicamos que si las peticiones llegan desde www.albertorcaballero.com, sean redirigidas al dominio sin el www. Por el momento no tenemos https pero en la redirección ya lo he dejado preparado así porque los vamos a instalar en cuanto terminemos de configurar Nginx.
Guardamos y habilitamos el site con el siguiente comando en el que añadimos el archivo que acabamos de editar y creamos un enlace simbólico a la carpeta sites-enabled:
sudo ln -s /etc/nginx/sites-available/albertorcaballero.com /etc/nginx/sites-enabled
Para comprobar que todo ha ido bien lanzamos el siguiente comando:
sudo nginx -t
Si todo ha ido bien reiniciamos Nginx:
sudo systemctl restart nginx
Después damos permisos a Nginx para el Firewall
sudo ufw allow 'Nginx Full'
Ahora debéis ir a la web donde comprásteis el dominio y realizar los cambios correspondientes para que el dominio apunte a la ip del servidor. Si no tenéis muy claro como hacer el proceso seguro que vuestro proveedor tiene tutoriales o documentación para hacerlo.
Yo os dejo captura de como lo he hecho con namecheap que es donde tengo el dominio:
La parte que véis pixelada sería la ip de mi servidor.
Si todo ha ido bien, ya podéis escribir el dominio en vuestro navegador y os debería aparecer vuestro proyecto funcionando al 100%
Podríamos decir que ya está terminado pero no. Nos falta algo muy importante que es poner los certificados para poder usar https. Por suerte hoy en día se pueden conseguir de forma gratuita
Para ello en nuestro server lanzamos el siguiente comando para instalar Certbot que podemos utilizar para instalar certificados gratuitos y que estos se renueven de forma automática:
sudo snap install --classic certbot
Después vamos a generar los certificados con el siguiente comando. Recordad sustituir mi dominio por el vuestro:
sudo certbot --nginx --hsts --staple-ocsp --must-staple -d albertorcaballero.com -d www.albertorcaballero.com
Seguimos los pasos y listo, este comando editará la configuración de Nginx para que siempre redirija a https, así que ahora solo tenéis que recargar la página y ya se mostrará vuestro proyecto como una web segura 💪.
Listo, ya tenemos nuestro server configurado al 100%. El siguiente paso sería automatizar el deploy cada vez que subamos un cambio al repositorio. Si usais GitHub, os dejo este tutorial donde explico como hacerlo.
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.