Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mortenege/baa9c9a77bdfbf11202aac1cef8c829f to your computer and use it in GitHub Desktop.
Save mortenege/baa9c9a77bdfbf11202aac1cef8c829f to your computer and use it in GitHub Desktop.
Django Channels with Vagrant, Apache and Supervisor

Django Channels with Vagrant, Apache and Supervisor

This post describes how to set up a Django Channels environment in Vagrant using Apache as a reverse proxy. We are picking up from an environment we already configured, see this previous post. This guide will describe multiple steps of the process including:

  • How to install and configure Channels
  • Creating a simple Echo app that utilises websockets
  • Configuring Apache to route our websockets
  • Running Channels in a production-like environment

Step 1

Install and configure Channels for our Django project.

$ cd /vagrant/django
$ source venv/bin/activate
$ pip install -U channels

NOTE: If twisted fails referring to 'fatal error: Python.h: No such file or directory' make sure to install build-tools for python.

$ apt-get install build-essential python-dev --assume-yes

We modify mysite/settings.py to include channels.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    ...
    'channels'
]

Step 2

Create a new app called echo.

$ cd /vagrant/django/mysite
$ source ../venv/bin/activate
(venv)$ python manage.py startapp echo

In the new app-folder created we place consumers.py

from django.http import HttpResponse
from channels.handler import AsgiHandler
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)

and in mysite-folder we add routing.py

from channels.routing import route
channel_routing = [
    route("http.request", "echo.consumers.http_consumer"),
]

Finally we edit mysite/settings.py to include our channel layer.

CHANNEL_LAYERS = {
    "default": {
    "BACKEND": "asgiref.inmemory.ChannelLayer",
    "ROUTING": "mysite.routing.channel_routing",
    },
}

Now when we run Django we can see that Channels is runing as our backend with _inmemory _transport layer, listening to http.request-channel.

(venv)$ python manage.py runserver 127.0.0.1:8080
[...]
Django version 1.10.5, using settings 'mysite.settings'
Starting Channels development server at http://127.0.0.1:8080/
Channel layer default (asgiref.inmemory.ChannelLayer)
Quit the server with CONTROL-C.
2017-02-10 02:18:38,726 - INFO - worker - Listening on channels http.request, websocket.connect, websocket.disconnect, websocket.receive
2017-02-10 02:18:38,732 - INFO - server - Using busy-loop synchronous mode on channel layer
2017-02-10 02:18:38,733 - INFO - server - Listening on endpoint tcp:port=8080:interface=127.0.0.1

Navigate to http://127.0.0.1:4567/helloworld! to see the results.

Step 3

Configure 001-mysite.conf to route all websocket traffic to Channels.

<VirtualHost *:80>
 ProxyPass "/ws/" "ws://127.0.0.1:8080/"
 ProxyPassReverse "/ws/" "ws://127.0.0.1:8080/"
 ProxyPass "/" "http://127.0.0.1:8080/"
 ProxyPassReverse "/" "http://127.0.0.1:8080/"
</VirtualHost>

We add a websocket consumer to consumers.py.

def ws_message(message):
    message.reply_channel.send({
        "text": message.content['text'],
    })

And add a route to routing.py

route("websocket.receive", "echo.consumers.ws_message"),

Navigate back to any url on our server, open the js console and paste the following:

socket = new WebSocket("ws://" + window.location.host + "/ws/");
socket.onmessage = function(e) {alert(e.data);}
socket.onopen = function() {socket.send("hello world");}
if (socket.readyState == WebSocket.OPEN) socket.onopen();

Step 4

Add views, urls and logic for a simple echo-page. We start by creating a template echo/templates/echo/echo.html.

<!DOCTYPE html>
<html>
<head>
 <title>Echo</title>
</head>
<body>
<h2>This page will hopefully <i>alert</i> "hello world" in the browser.</h2>
<script>
 socket = new WebSocket("ws://" + window.location.host + "/ws/");
 socket.onmessage = function(e) {alert(e.data);};
 socket.onopen = function() {socket.send("hello world");};
 if (socket.readyState == WebSocket.OPEN) socket.onopen();
</script>
</body>
</html>

Then we create a view that renders this template, echo/views.py.

from django.shortcuts import render
def index(request):
    return render(request, 'echo/echo.html')

We now need to route an url to our view.

# in _echo/urls.py_ from django.conf.urls import url
from . import views
urlpatterns = [
    url(r'^/pre>, views.index, name='index'),
]

_# in mysite/urls.py_
from django.conf.urls import include, url
urlpatterns = [
    url(r'^', include('echo.urls')),
]

Not to forget. We also need to actvate our app in mysite/settings.py.

INSTALLED_APPS = [
    'django.contrib.admin',
    [...]
    'channels',
    'echo',
]

Now when we navigate to http://127.0.0.1:4567/ we see that our echo service is working.

Migrating to production-like environment

Running Django Channels through the runserver command is not considered good practice; instead we are going to use Daphne as our interface server and run multiple workers to handle our requests. As we now split the logic into multiple processes we need some form of transport-layer where we can communicate between the interface server and the workers. For transparancy we are going to run with asgi_ipc.

$ source venv/bin/activate
(venv)$ pip install asgi_ipc

In settings.py we make IPC our default CHANNEL_LAYER.

"default": {
    "BACKEND": "asgi_ipc.IPCChannelLayer",
    "ROUTING": "mysite.routing.channel_routing",
    "CONFIG": {
        "prefix": "mysite",
    },
},

Now if we used runserver Channels would use the IPC layer instead of inmemory communication. To run our interface server Daphne we first create a file very similar to wsgi.pycalled asgi.py.

import os
from channels.asgi import get_channel_layer
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
channel_layer = get_channel_layer()

Now we configure Supervisor to run our Daphne interface server on port 8080 and also four workers.

[program:Daphne]
environment=PATH="/vagrant/django/venv/bin"
command=/vagrant/django/venv/bin/daphne -p 8080 mysite.asgi:channel_layer
directory=/vagrant/django/mysite
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/tmp/daphne.out.log

[program:Worker]
environment=PATH="/vagrant/django/venv/bin"
command=python manage.py runworker
directory=/vagrant/django/mysite
process_name=%(program_name)s_%(process_num)02d
numprocs=4
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/tmp/workers.out.log

We reload Supervisor and check what is running.

(venv)$ supervisorctl status 
Daphne RUNNING pid 3056, uptime 0:03:45 
Worker:Worker_00 RUNNING pid 3057, uptime 0:03:45 
Worker:Worker_01 RUNNING pid 3058, uptime 0:03:45 
Worker:Worker_02 RUNNING pid 3059, uptime 0:03:45 
Worker:Worker_03 RUNNING pid 3060, uptime 0:03:45

For the user, nothing has changed. We still load a webpage and are able to communicate with Channels over websockets.

Conclusion

We have successfully created a Django Channels app that runs websockets and normal HTTP requests. It is still running behind an Apache server configured as a reverse proxy. It is also still running as a child process of Supervisor that monitors our backend server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment