Instalar web apps Flask como servicios de Linux (Ubuntu, nginx, uwsgi)


Es un tema repetitivo el instalar web apps hechas en Flask como servicios de linux, así que he creado esta chuleta como recordatorio.

Está basada en Ubuntu 18.04 LTS, pero se puede adaptar fácilmente a cualquier otra distro.

Primero instalaremos el software que necesitamos tanto en el SO como en el entorno de ejecución. Después configuraremos los servicios.

Suelo usar esta estructura de directorio para las aplicaciones Flask:

  • ejemplo
    • venv
    • ejemplo
      • uwsgi.ini
      • log
      • static
      • models
      • project
      • lib

Todo el proyecto Flask está en ejemplo/ejemplo. El entorno virtual está en ejemplo/venv. La configuración de uwsgi está en ejemplo/ejemplo/uwsgi.conf. En esta chuleta usaremos también los directorios log para el log de uwsgi y static que es el mismo static de Flask.

Instalación y manejo del software

Inicio

sudo apt update

Comprobar que tenemos abiertos los puertos de ssh, http, https y si queremos, el de Postgre.

Comprobar versión de python python3 -V. Si es necesario, subirla con sudo apt-get upgrade python3.

Nos hará falta el compilador para instalar uwsgi

sudo apt install build-essential python3-dev

Gestionar servicios

Recordar la sintaxis de systemctl

sudo systemctl {status, start, stop, restart, reload, enable, disable} nombre_servicio

Para ver un listado de los módulos

sudo systemctl list-units

Instalar Nginx

Instalar sudo apt install nginx

Comprobar systemctl status nginx Si hay errores de configuración sudo nginx -t

Ver página de inicio en http://ip_servidor

Logs en /var/log/nginx/error.log

Nota acerca del monitorizado de logs

Una forma particularmente útil de hacerlo es abriendo un shell y tail -f nombre_log.log.

Configurar los servicios

Directorio del servicio

Cada servicio correrá en su propio directorio donde tendremos instalados los scripts de arranque y el entorno virtual de ejecución python.

cd ~
mkdir ejemplo

Creación del entorno de ejecución python

Instalamos virtualenv con sudo apt install virtualenv. Creamos el entorno virtual

cd ejemplo
virtualevn -p python3 venv

Probamos que todo esté ok.

. venv/bin/activate
python -V

Con el entorno activado, instalamos el uwsgi que necesita las librerías de desarrollo de python3 instaladas anteriormente.

sudo apt-get install libpcre3 libpcre3-dev
pip install uwsgi

Configuración del uwsgi

En este fichero controlamos la comunicación entre nginx y la aplicación via uwsgi.

/home/ubuntu/ejemplo/ejemplo/uwsgi.ini

[uwsgi]
module = uwsgi:app
lazy-apps = true
master = true
processes = 5

socket = /tmp/ejemplo.sock
chmod-socket = 660
vacuum = true

die-on-term = true

# socket = :8080
virtualenv = /home/ubuntu/ejemplo/venv
chdir = /home/ubuntu/ejemplo/ejemplo

logto = /home/ubuntu/ejemplo/ejemplo/log/uwsgi.log

Donde los parámetros más importantes son lazy-apps que controla el modo en que se cargan las aplicaciones, processes que controla el número de procesos que se lanzarán simultáneamente y que debe tunearse a mano (más procesos, más memoria pero mejor rendimiento) y socket que es el socket donde en realidad se conectará nginx con uwsgi.

Adicionalmente, podemos cambiar la localización del log de todo lo que hace el uwsgi, es interesante porque podemos localizar problemas rápidamente. Evidentemente, el directorio debe existir.

Configuración del nginx

Vamos al directorio de configuración de nginx y editamos el fichero del sitio

/etc/nginx/sites-available/ejemplo.com


server {
    listen 80;
    server_name ejemplo.ejemplo.com;
    return 301 https://$host$request_uri;
}


server {
	listen 443;

	ssl on;
	ssl_certificate /home/ubuntu/server/ejemplo.ejemplo.cer;
	ssl_certificate_key /home/ubuntu/server/server.key;

	server_name ejemplo.ejemplo.com;
	
	location /static {
    		root /home/ubuntu/ejemplo/ejemplo;
	}	
	
	location / {
		include uwsgi_params;
		uwsgi_pass unix:/tmp/ejemplo.sock;
	}
}

En la primera entrada server enrutamos las peticiones al 80 hacia el 443. En la segunda configuramos las rutas a los certificados y la ruta estática a los recursos para Flask. Finalmente, en location / le decimos a nginx el socket donde debe enrutar las peticiones, es decir, cuando entre una petición a ejemplo.ejemplo.com la enrutará al socket destrás del cuál está el uwsgi.

Ahora creamos el enlace en sites-enabled

sudo ln -s /etc/nginx/sites-available/ejemplo.com /etc/nginx/sites-enabled/

Y recargamos

sudo systemctl restart nginx Ahora podemos probar el sitio. Para mayor seguridad, descomentamos la línea de nginx.conf que puede causar problemas en multisite

sudo vi /etc/nginx/nginx.conf

Buscar server_names_hash_bucket_size y descomentarla, dejar valor de 64.

Comprobamos sintaxis en las configuraciones

sudo nginx -t

Reiniciamos el servicio

sudo systemctl restart nginx

Probamos https://ejemplo.com

Certificados

Regular

Creamos el csr con

mkdir ~/server 
cd ~/server
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out ejemplo.csr

Recordar poner como nombre el DNS del servidor.

Si tenemos un certificado del tipo *.ejemplo.com, poner este como nombre (con el asterisco).

Pedimos el certificado (en thawte, por ejemplo). Descargamos todos los ficheros y vemos que tenemos el certificado del sitio más los intermedios, en Nginx podemos usar la cadena, enlazando un certificado detrás de otro, primero el del sitio y seguidamente los intermedios. Es decir, primero el sitio, después la autoridad que firma el sitio, después la autoridad que firma el anterior, etc.

cat ssl_certificate.cer IntermediateCA.cer crossRootCA.cer >> bundle.cer

Ahora, en el fichero de configuración ponemos

	ssl on;
	ssl_certificate /home/ubuntu/server/bundle.cer;
	ssl_certificate_key /home/ubuntu/server/server.key;

Autofirmado

Si queremos usar un certificado auto firmado podemos hacer

sudo mkdir /etc/nginx/ssl
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt

Y cambiar la ruta del certificado en /etc/nginx/sites-available/ejemplo.com

Letsencrypt

Hay que seguir las instrucciones de https://certbot.eff.org/lets-encrypt/ubuntubionic-nginx. Básicamente nos bajamos y ejecutamos el certbot.

sudo apt-get update
sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot python-certbot-nginx
sudo certbot certonly --nginx

El directorio de instalación de los certificados es /etc/letsencrypt/live/ejemplo.com. Una vez terminado todo, comprobar que el certificado se autorenovará con

sudo certbot renew --dry-run

Editamos ejemplo.com para que busque los certificados de letsencrypt

/etc/nginx/sites-available/ejemplo.com

...
ssl on;
ssl_certificate /etc/letsencrypt/live/ejemplo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem; 
...

Comprobamos todo

sudo nginx -t
sudo service nginx restart

nginx.service: Failed to read PID from file /run/nginx.pid: Invalid argument

Si tenemos este error en /var/log/syslog con el arranque de nginx en ubuntu, podemos probar este workarround:

sudo su
mkdir /etc/systemd/system/nginx.service.d
printf "[Service]\nExecStartPost=/bin/sleep 0.1\n" > /etc/systemd/system/nginx.service.d/override.conf
systemctl daemon-reload
systemctl restart nginx

Instalación de la aplicación

En este caso, primero instalamos los requerimientos para el driver de postgre y de weasyprint. Después clonamos el proyecto desde github.

sudo apt-get install libpq-dev
sudo apt-get install build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info
cd ~/ejemplo
git clone https://github.com/ejemplo/ejemplo
. venv/bin/activate
pip install Psycopg2
pip install weasyprint
pip install -r requirements.txt

Una vez hecho esto podemos arrancar la aplicación en local para ver si todo está correcto.

# database connection string
export SQLALCHEMY_DATABASE_URI="postgresql+psycopg2://user:pass@localhost:5432/dbname"

# primera vista después de login
export SECURITY_POST_LOGIN_VIEW='/ejemplo/'

# log level critical, error, warning, info, debug
export LOG_LEVEL=DEBUG
export LOG_ROOT_NAME='ejemplo'
export LOG_FILE=./log/error.log

# development server
export HOST='0.0.0.0'
export PORT=5555
export USE_RELOADER=1

# información de la empresa
export COMPANY_NAME='My company'
export APP_NAME='My app name'

# requerido por weasyprint
export LC_ALL=es_ES.UTF-8
export LANG=es_ES.UTF-8

export RUN_MODE=DEVELOPMENT

export FLASK_APP=ejemplo.py
flask

Creación del punto de montaje uwsgi

Creamos el fichero uwsgi.py con algo similar a

from server import main_proc

app=main_proc()

Donde main_proc es una función que devuelve un objeto del tipo Flask app.

Probamos el uwsgi

Ahora lanzamos el uwsgi, con el entorno activado, para probar:

/home/ubuntu/ejemplo/venv/bin/uwsgi --ini /home/ubuntu/ejemplo/ejemplo/uwsgi.ini

Comprobaremos que esté todo correcto en ./log/uwsgi.log.

Notas acerca de los mensajes de uwsgi

Puede que el log nos muestre varios mensajes de advertencia, entre ellos:

  1. ** Python threads support is disabled. You can enable it with –enable-threads **
  2. !!! no internal routing support, rebuild with pcre support !!!

El primero nos indica que si arrancamos un threads desde un proceso, estos se bloquearán mientras no se suelte el main thread. Por lo tanto, si usamos threads es mejor usar la opción --enable-threads, si no los usamos es mejor no activarla por temas de rendimiento.

El segundo indica problemas con el build del wsgi. Posiblemente no se insalaron las librerías libpcre antes del build. Hay que reinstalar y recompilar con --no-cache.

sudo apt-get install libpcre3 libpcre3-dev
pip install uwsgi -I --no-cache-dir

Script de arranque

Los servicios se configuran mediante la creación de un script en /etc/systemd/system/ejemplo.service.

Atención a cada una de las secciones de configuración.

/etc/systemd/system/ejemplo.service

[Unit]
Description=Nombre descriptivo del servicio
After=network.target

[Service]
User=ubuntu
Group=www-data
Restart=always
RestartSec=5
StartLimitIntervalSec=0
WorkingDirectory=/home/ubuntu/ejemplo
Environment="PATH=/home/ubuntu/ejemplo/venv/bin"
Environment="FLASK_ENV=development"
Environment="RUN_MODE=PRODUCTION"
Environment="LC_ALL=es_ES.UTF-8"
Environment="LANG=es_ES.UTF-8"
Environment="SQL_DEBUG=0"
Environment="SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://user:pass@dbserver:5432/dbname"
Environment="SECURITY_POST_LOGIN_VIEW=/example/" # first page after login
Environment="LOG_LEVEL=WARNING"
Environment="LOG_ROOT_NAME=example"
Environment="COMPANY_NAME=My company"
Environment="APP_NAME=my app name"
Environment="LOG_FILE=/var/log/ejemplo.log"
LimitNOFILE=50000
ExecStart=/home/ubuntu/ejemplo/venv/bin/uwsgi --ini /home/ubuntu/ejemplo/ejemplo/uwsgi.ini


[Install]
WantedBy=multi-user.target

En Unit vemos que el servicio se arranca después de network. El StartLimitIntervalSec es el tiempo de retraso. En Service, el Restart y RestartSec controlan si el servicio debe reiniciarse después de una caida y en qué tiempo de retraso entre la caida y el reinicio. WorkingDirectory es el directorio desde donde se arrancará la aplicación. Environment son variables que se exportarán en el proceso. Es importante en python que el PATH esté mapeado al directorio del intérprete del entorno virtual. Finalmente, ExecStart es el script de arranque de Flask.

LimitNOFILE controla el número de ficheros abiertos simultáneamente, este número suele ser muy bajo por defecto y nos puede crear problemas en cualquier momento siendo mejor aumentarlo.

ExecStart apunta al puente uwsgi que tendremos en el entorno de ejecución anteriormente creado, el –ini apunta al fichero de inicio que lucirá más o menos como en la sección siguiente.

Tenemos que crear y cambiar permisos del fichero de log de la aplicación LOG_FILE. Notar que el log del uwsgi va por otra parte.

sudo touch /var/log/ejemplo.log
sudo chmod 666 /var/log/ejemplo.log

Nota

Si estamos instalando otro tipo de servicio, posiblemente User y Group seran ubuntu y el ExecStart será parecido a:

ExecStart=/bin/sh /home/ubuntu/aplicacion/run.sh`

Enablar el servicio

Para que el script arranque al principio

sudo systemctl enable /etc/systemd/system/ejemplo.service

Arrancamos ahora con

sudo systemctl start ejemplo.service

Y comprobamos errores en el log de uwsgi

tail -n 50 /home/ubuntu/ejemplo/ejemplo/log/uwsgi.log

en el de nginx

tail -n 50 /var/log/nginx/error.log

en el de sistema

tail -n 50 /var/log/syslog

y en el de aplicación

tail -n 50 /var/log/ejemplo.log

Post-instalación

Comprobación de la zona horaria

Comprobamos la zona horaria con timedatectl, si no está correcta, la fijamos con

sudo timedatectl set-timezone Europe/Madrid

Y volvemos a pasar timedatectl para comprobar.