Django: peticiones wsgi y asgi simultaneas (htmx y websockets)

Muy feliz de finalmente tenerlo funcionando! no sé si es la mejor forma, pero la cuestión es que funciona :D

Estos días me estoy peleando en hacer funcionar un chatbot con stream de openai sobre django. Me ha llevado un par de días de hacerlo funcionar pero aquí lo tenemos :D

Básicamente después de perderme entre decenas de páginas de documentación y abrir y cerrar otras decenas más de pestañas del navegador, lo que terminé haciendo fue localizar una aplicación que sí hacía lo que yo quería y luego la he implementado a mi aplicación. La aplicación que funcionaba like a charm en local es Django OpenAI-Powered Chatbot with HTMX Streaming. También recomiendo esta otra documentación (Learn to use Websockets with Django by building your own ChatGPT) que si eres relativamente nuevo con esto de htmx y websockets te va a aclarar mucho el tema :)

Inicié implementando el manual de saaspegasus pero no funcionaba. Así que he decidido directamente integrar el código de django_chatbot a mi aplicación y tampoco funcionaba… hasta que he visto lo de daphne! El python manage.py runserver de toda la vida no es síncrono pero daphne si, y lo que permite es activar las conexiones asíncronas. Al aplicar esto, todo ha empezado a funcionar :D

Después ha venido la parte de ponerlo en producción que de nuevo me he perdido entre múltiples páginas pero finalmente lo que sacado y aquí lo dejo para saber como implementarlo la próxima vez y también para otro que llegue aquí después de mucho navegar. En este post me centro sólo en la parte de poner a funcionar la parte de producción.

Primero de todo, tendremos que añadir gunicorn a nuestro proyecto (y daphne para el entorno de desarrollo) y lo guardamos en requirements.txt para que no se nos olvide. No es necesario instalarlos por apt porqué lo estamos haciendo con pip en el entorno venv.

A continuación, en el servidor de producción creamos el fichero /etc/systemd/system/petitsuis_gunicorn.service con el siguiente contenido:

# vi /etc/systemd/system/petitsuis_gunicorn.service
[Unit]
Description=Gunicorn daemon for petitsuis
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/html/petitsuis
ExecStart=/var/www/html/petitsuis/venv/bin/gunicorn petitsuis.asgi:application -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 --workers 4 --threads 2 --log-level info
Restart=always

[Install]
WantedBy=multi-user.target

A continuación recargamos la configuración de systemd, paramos y arrancamos el servicio

# systemctl daemon-reload
# systemctl stop petitsuis_gunicorn
# systemctl start petitsuis_gunicorn

Con esto tenemos gunicorn sirviendo el contenido asgi en el puerto 8000.

Hasta el momento los servidores con django los configuro con uwsgi tal como expliqué hace años en este post Instalar entorno de producción de python. Así que de momento y como no me fio mucho de momento de gunicorn voy a dejar uwsgi como siempre para toda la aplicación y gunicorn para /ws/ que es donde está el websocket. Dejo comentada la configuración del backend nginx que también funciona.

Referencia: WebSocket proxying (nginx.org)

BACKEND NGINX (DONDE RESIDE LA APLICACIÓN)

# vi /etc/nginx/sites-available/petitsuis
server {
    listen 80;
    server_name _;

    set_real_ip_from 192.168.XXX.0/24;
    real_ip_header X-Real-IP;
    real_ip_recursive on;

    client_max_body_size 200M;

    fastcgi_read_timeout 300;
    proxy_read_timeout 300;

    # wsgi application through uwsgi
    location / {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/uwsgi-petitsuis.sock;
        uwsgi_read_timeout 300;
    }

    # wsgi application through gunicorn
    #location / {
    #    proxy_pass http://localhost:8000;
    #    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    #    proxy_set_header Host $host;
    #    proxy_redirect off;
    #}

    location /ws/ {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }

    # /static
    location /static/ {
        autoindex off;
        root /var/www/html/petitsuis/web/;
    }
}

FRONTAL NGINX (DONDE ESTÁN LOS SSL Y ESTÁ EXPUESTO A INTERNET)

Ahora lo siguiente será modificar también el fichero de configuración de nginx del nginx frontend que es donde están todos los certificados ssl y también es la máquina expuesta a internet

# vi /etc/nginx/sites-enabled/petitsuis.capa8.net

# INSTANCES petitsuis
server {
    listen 80;
    listen [::]:80;

    server_name petitsuis.capa8.net;
    return 301 https://petitsuis.capa8.net$request_uri;

    #root /var/www/html;

    include snippets/certbot.conf;
}

# 2024/08/16 - Petitsuis (navalla suïssa de petites cosetes)
server {
        server_name petitsuis.capa8.net;

        include snippets/certbot.conf;

        listen 443 ssl;
        listen [::]:443 ssl http2;
        ssl_certificate /etc/letsencrypt/live/petitsuis.capa8.net/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/petitsuis.capa8.net/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_trusted_certificate /etc/letsencrypt/live/petitsuis.capa8.net/chain.pem;

        location / {
                proxy_set_header  Host $host;
                proxy_set_header  X-Real-IP $remote_addr;
                proxy_set_header  X-Forwarded-Proto https;
                proxy_set_header  X-Forwarded-For $remote_addr;
                proxy_set_header  X-Forwarded-Host $remote_addr;
                proxy_set_header Accept-Encoding "";
                proxy_pass http://192.168.100.37:80;

                # WebSocket support
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }
}     

El secreto de la masa está en las tres líneas de configuración debajo de “WebSocket support”.

En el servidor de backend he creado un script que se llama fast.sh que sirve para descargar el código del repositorio y reiniciar todos los servicios necesarios (el script no instala las dependencias nuevas y tienes que hacerlo a mano con pip install -r requirements.txt):

# fast.sh
#!/bin/bash

cd /var/www/html/petitsuis && source venv/bin/activate && git pull && python manage.py makemigrations && python manage.py migrate && cd .. && chown -R www-data:www-data * && service uwsgi restart && service nginx restart

supervisorctl stop petitsuis-celery
supervisorctl start petitsuis-celery

systemctl stop petitsuis_gunicorn
systemctl start petitsuis_gunicorn

Y listos! aquí queda funcionando la cosa :)

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *

Aquest lloc utilitza Akismet per reduir els comentaris brossa. Apreneu com es processen les dades dels comentaris.