Configuración uWSGI + NGINX + FLASK

¿Qué es uWSGI?

uWSGI es un servidor de aplicaciones para aplicaciones Python que se comunica con los servidores web a través del protocolo WSGI. WSGI, o Web Server Gateway Interface, es un estándar en Python para permitir la comunicación entre servidores web y aplicaciones web. Es extremadamente flexible y permite una gran variedad de configuraciones, desde simples aplicaciones de un solo archivo hasta complejas aplicaciones con múltiples procesos y subprocesos.

En este ejemplo lo vamos a utilizar desde docker-compose y dos contenedores, uno para nginx y otro para python y uwsgi.

Estructura del proyecto

mi_proyecto/ 
├── docker-compose.yml 
├── app/ 
           └── app_api.py 
├── infrastructure/
      ├── app_python/
           └── requirements.txt
           └── Dockerfile
           └── uwsgi.ini
      ├── config_nginx/
           └── app_python/nginx.conf
├── nginx_logs/ 
           └── access.log
           └── error.log 
├── .env
├── .gitignore
├── README

Docker-compose

Empezamos con docker-compose para configurar todo lo necesario:

version: '3.7'
services:
  python_container:
    build: 
      context: ./infrastructure/app_python
      dockerfile: Dockerfile
    ports:
      - ${API_FLASK_PORT}:4450
    volumes:
      - ./app:/app
    networks:
      - shared_network
    tty: true

  nginx:
    image: nginx
    ports:
      - 8450:80
    volumes:
      - ./infrastructure/config_nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx_logs:/var/log/nginx
    networks:
      - shared_network

networks:
  shared_network:
    external: true

volumes:
  db-admin-data:
    driver: local
    name: db-admin-data

Configuración Dockerfile

# Ejemplo de python, podeis usar el que queráis, 
#  lo único que dependiendo del linux pueden cambiar los comandos, recordarlo.
FROM python:3.8.10

WORKDIR /app

# Install uWSGI
RUN apt-get update -y
RUN apt install net-tools
# Se necesita instalar este primero, sino me daba error que no lo encontraba el plugin.
RUN apt-get install -y uwsgi-plugin-python3
# Instalando uwsgi
RUN pip install uwsgi
# Copiando archivo de configuración
COPY uwsgi.ini /etc/uwsgi/

# uWSGI Python plugin
# Especifica el plugin que uWSGI debe usar
ENV UWSGI_PLUGIN python3

# Especifica la ubicación del archivo de configuración de uWSGI
ENV UWSGI_INI /etc/uwsgi/uwsgi.ini

# Estas son configuraciones específicas de uWSGI relacionadas con 
# el número de procesos que se deben ejecutar.
ENV UWSGI_CHEAPER 2
ENV UWSGI_PROCESSES 16

# Instalando monitorización uwsgi (luego más abajo lo veremos)
RUN pip install uwsgitop

## Instalando requirements
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Crear un grupo y usuario para no usar root
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser

# Usar el usuario creado en los futuros comandos
USER appuser

## Ejecuciones:
CMD uwsgi --ini /etc/uwsgi/uwsgi.ini

Configuración de la aplicación Flask

Vamos a considerar que tenemos un archivo Python, app_api.py, que contiene nuestra aplicación Flask:

from flask import Flask, request, jsonify

## Aplicación
app = Flask(__name__)

## Máxima capacidad de archivo a recibir
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024

@app.route('/hola')
def hola():
	return '<h1>La página que intentas buscar no existe</h1>'
   
def pagina_no_encontrada(error):
    return '<h1>La página que intentas buscar no existe</h1>'

app.register_error_handler(404, pagina_no_encontrada)

# Recordar que esto solo se usa si accedes ejecutando python.
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=4450)

Configuración de uWSGI

Para utilizar uWSGI, necesitas un archivo de configuración uwsgi.ini. En este ejemplo, lo hemos configurado de la siguiente manera:

[uwsgi]

## Usuario que ejecuta
uid = appuser
gid = appgroup

## Ubicación del plugin
plugins-dir = /usr/lib/uwsgi/plugins/
plugin = python3
## Especifica la ubicación de la aplicación WSGI.
module = app_api:app
## Nombre de la aplicación
callable = app

## Escuchar conexiones entrantes
# Para funcionar intennamente
#http = :4450
# Para funcionar desde el exterior
socket = :4450

## Procesos y hilos
processes = 4
threads = 2

## Monitorización
stats = /tmp/uwsgi-stats.sock

Con estas configuraciones yo probaría que todo funcionase como quieres antes de pasar al siguiente paso.

Configuración de nginx

Para que nginx pueda comunicarse con uWSGI, debes configurar un bloque de servidor en tu archivo nginx.conf. Aquí está un ejemplo:

# Indica nª de procesos de worker que se deben iniciar. Normalmente se configura al número de cores de la CPU.
worker_processes 1;

# Máximo número de conexiones simultáneas que puede manejar cada proceso worker.
events {
    worker_connections 1024;
}

http {
    # Este archivo contiene los tipos MIME que Nginx usará.
    include /etc/nginx/mime.types;
    # Tipo MIME por defecto.
    default_type application/octet-stream;

        # Oculta la versión de NGINX en las respuestas HTTP.
    server_tokens off;

    # Cabeceras de seguridad:
    add_header X-Frame-Options SAMEORIGIN;  # Protege contra clickjacking
    add_header X-Content-Type-Options nosniff;  # Protege contra ataques MIME
    add_header X-XSS-Protection "1; mode=block";  # Protección contra cross-site scripting
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; # Configuración de HSTS

    # Limitación de tasa para prevenir fuerza bruta o ataques DDoS (por ejemplo, para limitar a 5 solicitudes por minuto).
    limit_req_zone $binary_remote_addr zone=one:10m rate=5r/m;
    
    # Limitar número de conexiones simultáneas por IP (por ejemplo, a 10 conexiones por IP).
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn addr 10;

    # Permite envío de archivos directamente desde el sistema de archivos al socket de red.
    sendfile on;
    # Bloque que define el servidor upstream (en este caso, el servidor uWSGI).
    upstream app_server {
        # Dirección y puerto del servidor uWSGI.
        server python_mavm_light:4450;
    }

    # Bloque que define el servidor HTTP.
    server {
        # Puerto en el que el servidor escuchará las solicitudes -> el que quieres en el exterior va en el docker-compose
        listen 80;

        # Bloque que define cómo se deben manejar las solicitudes a una ubicación específica (en este caso, la raíz del servidor).
        location / {
            # Se usa para probarlo sin uwsgi
            #proxy_pass http://app_server; 

            # Se usa con uwsgi
            include uwsgi_params;
            uwsgi_pass app_server;

            # Establece el encabezado Host para el proxy.
            proxy_set_header Host $host;
            # Establece el encabezado X-Real-IP para el proxy.
            proxy_set_header X-Real-IP $remote_addr;
            # Establece el encabezado X-Forwarded-For para el proxy.
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            # Establece el encabezado Content-Type para el proxy.
            proxy_set_header Content-Type $http_content_type;
            # Establece el encabezado Content-Length para el proxy
            proxy_set_header Content-Length $http_content_length;
            # Define la versión de HTTP para el proxy.
            proxy_http_version 1.1;
            # Se usa para Websockets.
            proxy_set_header Upgrade $http_upgrade;
            # Se usa para Websockets.
            proxy_set_header Connection "Upgrade";

            # Control de manejo del tamaño de archivo recibido
            client_max_body_size 50M; 
            
            # Encabezados Cors, permite que el navegador realice las solicitudes
            # Si deseas restringir el acceso solo a orígenes específicos, reemplazar el * con el origen permitido.
            # Por ejemplo, add_header Access-Control-Allow-Origin http://ip_publica:20222;

            # Permitir el acceso: a todos.
            # add_header Access-Control-Allow-Origin *; ## A todos
            add_header Access-Control-Allow-Origin "web.com"; ## A tu misma web
            # Establece los métodos HTTP permitidos para las solicitudes externas.
            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
            # Establece los encabezados personalizados permitidos en solicitudes CORS.
            add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            # Define qué encabezados pueden ser expuestos en respuesta a solicitud CORS. 
            add_header Access-Control-Expose-Headers 'Content-Length,Content-Range';
        }
        # Carpeta de registros
        access_log /var/log/nginx/access.log;  
        error_log /var/log/nginx/error.log;
    }
}

En este archivo nginx.conf:

  • Definimos un servidor upstream app_server que apunta al socket uWSGI (python_contenedor:4450).
  • Configuramos el bloque del servidor para escuchar en el puerto 80 y pasar todas las solicitudes a uWSGI.
  • Le decimos donde tienen que ir los archivos log de acceso y error.

Detener uWSGI

Para ver si uWSGI se está ejecutando, puedes ejecutar ps aux en tu contenedor Docker, lo que te dará una salida similar a la siguiente:

sqlCopy code
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
appuser        1  0.2  1.1 104500 46112 pts/0    Ss+  08:56   0:00 uwsgi --ini /etc/uwsgi/uwsgi.ini
appuser        7  0.0  0.7 178232 31236 pts/0    Sl+  08:56   0:00 uwsgi --ini /etc/uwsgi/uwsgi.ini
appuser        9  0.0  0.7 178232 31236 pts/0    Sl+  08:56   0:00 uwsgi --ini /etc/uwsgi/uwsgi.ini

Para detener uWSGI, puedes enviar la señal TERM a uWSGI utilizando el comando kill:

bashCopy code
kill -TERM <PID>

Reemplaza <PID> con el ID del proceso uWSGI principal (el de menor número). La señal TERM le pide a uWSGI que se cierre de forma segura, finalizando cualquier solicitud en curso antes de salir.

Si necesitas detener uWSGI inmediatamente y por completo, puedes enviar la señal QUIT en su lugar:

bashCopy code
kill -QUIT <PID>

Ten en cuenta que esta señal hará que uWSGI se cierre de inmediato, lo que puede interrumpir las solicitudes en curso.

Supervisar el estado de uWSGI

  • Desde dentro del contenedor:

uWSGI proporciona una herramienta llamada uwsgitop que puedes usar para supervisar el estado de tus procesos de uWSGI. Para usar uwsgitop, debes habilitar las estadísticas de uWSGI en tu archivo de configuración de uWSGI:


stats = /tmp/uwsgi-stats.sock

Luego, puedes ejecutar uwsgitop /tmp/uwsgi-stats.sock para ver el estado de uWSGI en tiempo real.

  • Desde fuera del contenedor

Docker tiene el comando docker logs: Si estás ejecutando uwsgi dentro de un contenedor Docker, puedes usar el comando docker logs para ver la salida del contenedor.

Puedes hacerlo consultando el log del container-id, recuerda que para saber la id puedes buscarlo con “docker ps” y buscas el contenedor python que es donde está el uwsgi. Para consultar el log con el siguiente comando:

docker logs <container-id>

Ten en cuenta que verás todos los print del contenedor, en el caso de que el contenedor python tenga algún print también te lo mostrará entre medio de los print de uwsgi.

Pero lo recomiendo para que veas los avisos o errores que puedas tener! TomaNota!

Un ejemplo que verás en el log:

** Starting uWSGI 2.0.21 (64bit) on [Thu Jul 10 14:27:23 2023] ***
compiled with version: 8.3.0 on 10 July 2023 14:09:00
os: Linux-5.19.0-1028-aws #29~22.04.1-Ubuntu SMP Tue Jun 20 19:12:11 UTC 2023
nodename: 0e06btyd42d2
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 2
current working directory: /app
detected binary path: /usr/local/bin/uwsgi
your memory page size is 4096 bytes
detected max file descriptor number: 1048576
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to TCP address :4450 fd 3
Python version: 3.8.10 (default, Jun 23 2021, 15:19:53) [GCC 8.3.0]
Python main interpreter initialized at 0x58084ef654b0
python threads support enabled
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 416880 bytes (407 KB) for 8 cores
*** Operational MODE: preforking+threaded ***
WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x58084ef654b0 pid: 6 (default app)
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 6)
spawned uWSGI worker 1 (pid: 8, cores: 2)
spawned uWSGI worker 2 (pid: 9, cores: 2)
*** Stats server enabled on /tmp/uwsgi-stats.sock fd: 16 ***
## Aquí se verán las peticiones a la web!
[pid: 8|app: 0|req: 1/1] 118.118.118.118 () {40 vars in 714 bytes} [Fri Jul 11 07:31:49 2023] GET /uu => generated 49 bytes in 8 msecs (HTTP/1.1 200) 2 headers in 79 bytes (1 switches on core 0)

ERRORES VARIOS
En caso de error 502:
– Comprobar la conexión entre nginx y uwsgi, con un simple ping nginx desde el contenedor de uwsgi sabrás si el error es por conexión entre contenedores.
– Comprobar que la ruta a la cual estás mandando la web esté correcta.
Error uwsgi plugin no found:
– Comprobar si existe el plugin, para ello desde dentro del contenedor buscamos con el comando:
find / -name ‘python3_plugin.so’
Si existe, podemos comprobar la ruta en el archivo uwsgi.ini y añadimos la ruta de la carpeta:
## Datos archivos y conexiones
plugins-dir = /usr/lib/uwsgi/plugins/
plugin = python3
Y en el DockerFile donde creamos la variable de entorno añadimos la ruta absoluta:
# uWSGI Python plugin
# As an env var to re-use the config file
ENV UWSGI_PLUGIN /usr/lib/uwsgi/plugins/python3_plugin.so


Espero que esto te resulte útil este post para tu configuración de nginx + uWSGI con Flask en Docker. Como siempre, asegúrate de probar todo en un entorno de desarrollo antes de implementarlo en producción y cualquier aportación es bienvenida.

Salu2!

1 Comments

  1. Pingback: NGINX + SSL, python + uWSGI en Docker – Tomo nota!

Leave Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *