Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serving Django Channels In Production-like Setting With Apache Directing Traffic

I have been playing around with Django Channels for a a little bit and I am stuck on how to get it to work outside of a local development server setting. Before someone just pastes a documentation page to me, I have exhausted my search of the Django Channels documentation and everywhere else I could find. I can get the local setup to work fine, but not externally. My philosophy over years has been to never develop with the Django development server ever under any circumstances anyways because of exactly this kind of situation.

So here it is:

I have a django site that has been served by apache for years and uses LDAP user authentication (this matter is beyond my control and pay grade). I've installed Django Channels, asgi_redis, redis-server, and interface server Daphne comes with Django Channels automatically. I'm also working in CentOS 6/7.

I have so far worked out that I need to use apache as a reverse proxy to talk to ASGI/Daphne, but I just cannot find the info I need or figure it out myself, apparently.

Here's the closest configuration I can figure out. I have my apache configuration file setup as (the URL is external since my development server is remote; sensitive info is edited, of course):

< VirtualHost *:80 >
    # Django Channels
    ProxyPass        "/ws/" "ws://192.168.xx.xx/"
    ProxyPassReverse "/ws/" "ws://192.168.xx.xx/"
    ProxyPass        "/"    "http://192.168.xx.xx/"
    ProxyPassReverse "/"    "http://192.168.xx.xx/"

    WSGIDaemonProcess dashboard_jnett python-path=/home/jnett/dashboard_jnett:/home/jnett/airview_env/lib/python2.7/site-packages
    WSGIScriptAlias /dashboard_jnett /home/jnett/dashboard_jnett/apache/dashboard_jnett.wsgi process-group=dashboard_jnett
    <Directory /home/jnett/dashboard_jnett>
        AuthType Basic
        AuthName "Web Utilities"
        AuthBasicProvider ldap
        AuthGroupFile /dev/null
        require valid-user
        AuthLDAPBindDN "uid=authenticate,ou=system,dc=intranet,dc=row44,dc=com"
        AuthLDAPBindPassword "xxxxxxx"
        AuthLDAPURL ldap://192.168.xx.xx/ou=users,dc=intranet,dc=row44,dc=com?cn??(&(objectclass=inetOrgPerson)(member=cn=status))
        Require ldap-filter objectClass=inetOrgPerson
    </Directory>

    Alias /static/dashboard_jnett /var/www/html/static/dashboard_jnett

    <Directory /var/www/html/static/dashboard_jnett>
        AllowOverride None
        Require all granted
        Options FollowSymLinks
    </Directory>
</VirtualHost>

where I access the site root in-browser via: http://192.168.xx.xx/dashboard_jnett/

In my project code, I then have my asgi.py file: import os import channels.asgi

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings_jnett")
channel_layer = channels.asgi.get_channel_layer()

In that settings file settings_jnett.py reference in the ASGI file there I have:

import asgi_redis
...
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "asgi_redis.RedisChannelLayer",
        "CONFIG": {
            "hosts": [os.environ.get('REDIS_URL', 'redis://192.168.xx.xx:6379')],
            "prefix": u"dashboard_jnett",
        },
        "ROUTING": "routing.channel_routing",
    },
}

as well as the appropriate packages added to INSTALLED_APPS, which I left out.

That does correctly point to the routing.py file, which contains:

from channels.routing import route
from channeltest.consumers import ws_connect, ws_message, ws_disconnect, http_consumer

channel_routing = [
    route("websocket.connect",    ws_connect),
    route("websocket.receive",    ws_message),
    route("websocket.disconnect", ws_disconnect),
    route("http.request",         consumers.http_consumer),
    #route("websocket.receive", "consumers.ws_message"),
]

which imports from this consumers.py file

from django.http import HttpResponse
from channels.handler import AsgiHandler

from channels import Group
from channels.sessions import channel_session

# Connected to websocket.connect
@channel_session
def ws_connect(message):

    # Accept connection
    message.reply_channel.send({"accept": True})

    # Work out room name from path (ignore slashes)
    room = message.content['path'].strip("/")

    # Save room in session and add us to the group
    message.channel_session['room'] = room
    Group("chat-%s" % room).add(message.reply_channel)

# Connected to websocket.receive
@channel_session
def ws_message(message):
    Group("chat-%s" % message.channel_session['room']).send({
        "text": message['text'],
    })

# Connected to websocket.disconnect
@channel_session
def ws_disconnect(message):
    Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)

def http_consumer(message):
    response = HttpResponse("Hello world! You asked for %s" % message.content['path'])
    for chunk in AsgiHandler.encode_response(response):
        message.reply_channel.send(chunk)

I have Daphne running in terminal with:

[[email protected] ~/dashboard_jnett ]$(airview_env)[[email protected] ~/dashboard_jnett ]$daphne -b 192.168.xx.xx asgi:channel_layer --port 6379
2017-08-23 18:57:56,147 INFO     Starting server at tcp:port=6379:interface=192.168.xx.xx, channel layer asgi:channel_layer.
2017-08-23 18:57:56,147 INFO     HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2017-08-23 18:57:56,147 INFO     Using busy-loop synchronous mode on channel layer
2017-08-23 18:57:56,148 INFO     Listening on endpoint tcp:port=6379:interface=192.168.xx.xx
2017-08-23 18:57:56,148 INFO     HTTPFactory starting on 6379
2017-08-23 18:57:56,148 INFO     Starting factory <daphne.http_protocol.HTTPFactory instance at 0x54aca28>

I have a working running in another terminal with:

[[email protected] ~/dashboard_jnett ]$python manage.py runworker
2017-06-14 20:46:47,988 - INFO - runworker - Using single-threaded worker.
2017-06-14 20:46:47,988 - INFO - runworker - Running worker against channel layer default (asgi_redis.core.RedisChannelLayer)
2017-06-14 20:46:47,989 - INFO - worker - Listening on channels http.request, websocket.connect, websocket.disconnect, websocket.receive

I have redis server running in another terminal with:

[[email protected] ~/dashboard_jnett ]$~jnett/redis-stable/src/redis-server 
...
10940:M 14 Jun 20:41:25.224 * The server is now ready to accept connections on port 6379

I am not even trying to utilize the web sockets yet--I am just trying to serve the normal HTTP traffic first, but I can only seem to get proxy errors from apache:

Proxy Error

The proxy server received an invalid response from an upstream server. The proxy server could not handle the request GET /dashboard_jnett/channeltest/.

Reason: Error reading from remote server

where the apache error log is giving me many lines like

[Wed Jun 14 21:39:52.718388 2017] [proxy_http:error] [pid 13123] (70007)The timeout specified has expired: [client 192.168.xx.xx:51814] AH01102: error reading status line from remote server 192.168.xx.xx:80 [Wed Jun 14 21:39:52.718426 2017] [proxy:error] [pid 13123] [client 192.168.xx.xx:51814] AH00898: Error reading from remote server returned by /dashboard_jnett/channeltest/

Has anyone successfully put such a setup into production? I'm hoping that if I can figure out how to get apache to direct traffic to Daphne properly for normal HTTP, I can find my way from there.

like image 323
roninveracity Avatar asked Jun 14 '17 21:06

roninveracity


1 Answers

I also struggled to get it run on my Raspberry, but finally I made it.

from https://mikesmithers.wordpress.com/2017/02/21/configuring-django-with-apache-on-a-raspberry-pi/ I got good advices.

Apache needs some further packages to serve pages from Django application.

sudo apt-get install apache2-dev
sudo apt-get install libapache2-mod-wsgi-py3

also MPM (Multi-Processing-Module) has to be set.

a2dismod mpm_prefork
a2enmod mpm_worker
service apache2 restart

create asgi.py example from Django channels docu

https://channels.readthedocs.io/en/latest/deploying.html#run-protocol-servers

In my case I also had to add sys path to my Project

"""
ASGI entrypoint. Configures Django and then runs the 
application
defined in the ASGI_APPLICATION setting.
"""

import os
import sys
import django
from channels.routing import get_default_application

sys.path.append("/home/pi/Dev/WeatherStation")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", 
"WeatherStation.settings")
django.setup()
application = get_default_application()

Now Daphne should not complain

daphne -p 8001 WeatherStation.asgi:application

Configure ASGI and Daphne for websockets in Apache, use Apache for HTTP requests. Apache acts like a reverse proxy, redirecting all the websocket requests to Daphne server which is running on a different port

leafpad /etc/apache2/sites-available/000-default.conf

and the content

...
        RewriteEngine on
        RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC,OR]
        RewriteCond %{HTTP:CONNECTION} ^Upgrade$ [NC]
        RewriteRule .* ws://127.0.0.1:8001%{REQUEST_URI} [P,QSA,L]


Alias /static /home/pi/Dev/WeatherStation/static
    <Directory /home/pi/Dev/WeatherStation/static> 
        Require all granted
    </Directory>

    <Directory /home/pi/Dev/WeatherStation/WeatherStation>
        <Files wsgi.py>
            Require all granted
        </Files>
    </Directory>

    WSGIDaemonProcess Dev python-path=/home/pi/Dev python-home=/home/pi/Dev/WSenv
    WSGIProcessGroup Dev
    WSGIScriptAlias / /home/pi/Dev/WeatherStation/WeatherStation/wsgi.py
</VirtualHost>

Make sure that Apache has access to your db and other stuff

chmod g+w ~/dvds/db.sqlite3
chmod g+w ~/dvds
sudo chown :www-data db.sqlite3
sudo chown :www-data ~/dvds

Restart Apache for these changes to take effect:

sudo service apache2 restart

Now you have a WSGI server running in Apache and a Daphne server for websockets

like image 148
Novaspace Avatar answered Sep 20 '22 13:09

Novaspace