This tutorial covers starting a basic Django project with django-channels and the deployment process.

Objectives

  • create a django project with simple websocket chat application
  • deploy it into production

Source code:

  • Django project repository: https://github.com/appliku/django-channels-tutorial

Project Setup

Download fresh Djangitos project template, rename the project folder and copy local development .env file.

curl -sSL https://appliku.com/djangitos.zip > djangitos.zip
unzip djangitos.zip


mv djangitos-master channels_tutorial
cd channels_tutorial
cp start.env .env

Add new packages to requirements.txt file:

channels==3.0.4
channels-redis==3.3.0
daphne==3.0.2
asgiref==3.3.4

Keep in mind that these are exact versions that proven to work, at the moment of writing this tutorial newer asgiref package caused this exception RuntimeError: no running event loop read more here: https://github.com/django/channels/issues/1713

Include channels into installed applications. In djangito/settings.py add channels to the THIRD_PARTY_APPS list:

THIRD_PARTY_APPS = [
    'import_export',
    'django_extensions',
    'rest_framework',
    'storages',
    'corsheaders',
    'djangoql',
    'post_office',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'crispy_forms',
    'channels',  # new
]

Replace contents of djangito/asgi.py with this:

import os
import django

from channels.routing import get_default_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangito.settings')
django.setup()
application = get_default_application()

Back in djangito/settings.py add the following line:

ASGI_APPLICATION = 'mychat.routing.application'

Add the definition of CHANNEL_LAYERS after REDIS_URL definition, so it looks like this:

# Redis Settings
REDIS_URL = env('REDIS_URL', default=None)

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [REDIS_URL],
        },
    },
}

Apply migrations with:

docker-compose run web python manage.py migrate

Run the project with:

docker-compose up

Create a chat app

Let's create an app that will hold our code for the chat room.

docker-compose run web python manage.py startapp mychat

Include the app into djangito/settings.py PROJECT_APPS variable:

PROJECT_APPS = [
    'usermodel',
    'ses_sns',
    'mychat',
]

Create a file mychat/consumers.py:

import json

from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer


class MyConsumer(WebsocketConsumer):
    def connect(self):
        async_to_sync(self.channel_layer.group_add)(
            'chat_room',
            self.channel_name
        )
        self.accept()

    def disconnect(self, close_code):
        async_to_sync(self.channel_layer.group_discard)(
            'chat_room',
            self.channel_name
        )

    def receive(self, text_data=None, bytes_data=None):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        async_to_sync(self.channel_layer.group_send)(
            'chat_room',
            {
                'type': 'chat_message',
                'message': message
            }
        )

    def chat_message(self, event):
        message = event['message']
        self.send(text_data=json.dumps(
            {
                'message': message
            }
        ))

Create a file mychat/routing.py:

from django.core.asgi import get_asgi_application

from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path

from mychat.consumers import MyConsumer

websocket_urlpatterns = [
    path(r'chat', MyConsumer.as_asgi()),
]
application = ProtocolTypeRouter({
    'http': get_asgi_application(),
    'websocket': URLRouter(websocket_urlpatterns),
})

In mychat/views.py we need to createa a view that will render the HTML and JS for our websockets chat:

from django.shortcuts import render


def room(request):
    return render(request, 'mychat/room.html', {})

Add it to mychat/urls.py:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.room, name='room'),
]

Now we need template for our view. Create the file mychat/templates/mychat/room.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room</title>
</head>
<body>
    <textarea id="chat-log" cols="100" rows="20"></textarea><br>
    <input id="chat-message-input" type="text" size="100"><br>
    <input id="chat-message-submit" type="button" value="Send">
    {{ room_name|json_script:"room-name" }}
    <script>
        const roomName = JSON.parse(document.getElementById('room-name').textContent);

        const chatSocket = new WebSocket(
            'wss://'
            + window.location.host
            + '/chat'
        );

        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.message + '\n');
        };

        chatSocket.onclose = function(e) {
            console.error('Chat socket closed unexpectedly');
        };

        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#chat-message-submit').click();
            }
        };

        document.querySelector('#chat-message-submit').onclick = function(e) {
            const messageInputDom = document.querySelector('#chat-message-input');
            const message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message
            }));
            messageInputDom.value = '';
        };
    </script>
</body>
</html>

Now add our view to the root URLConf in djangito/urls.py as the first item:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('mychat.urls')),  # new

        ...
        ]

You can now restart your docker-compose by pressing CTRL-C and restart it again:

docker-compose up

You should see something like this in the output:

web_1       | Using selector: EpollSelector
web_1       | Using selector: EpollSelector
web_1       | Watching for file changes with StatReloader
web_1       | Watching for file changes with StatReloader
web_1       | Performing system checks...
web_1       |
web_1       | System check identified no issues (0 silenced).
web_1       | September 12, 2021 - 07:37:42
web_1       | Django version 3.2.7, using settings 'djangito.settings'
web_1       | Starting ASGI/Channels version 3.0.4 development server at http://0.0.0.0:8060/
web_1       | Quit the server with CONTROL-C.
web_1       | HTTP/2 support not enabled (install the http2 and tls Twisted extras)
web_1       | Configuring endpoint tcp:port=8060:interface=0.0.0.0
web_1       | Listening on TCP address 0.0.0.0:8060

Now we need to edit Procfile because django-channels requires a worker and daphne instead of the gunicorn.

With gunicorn the PORT is passed via environment variable, but daphne doesn't respect env variable, so we have to pass it via argument in command line. Since Appliku uses docker-compose under the hood to spin up the command and passing variable in docker-compose command doesn't work let's make a separate bash script to launch daphne and put it in the web process.

Create a run-daphne.sh in the root of the project with the following command:

daphne djangito.asgi:application --port=$PORT --bind 0.0.0.0 -v2

Open the Procfile in the root of the project. We'll change the web process and add a daphneworker line so those lines look this way, the rest of the lines(for release and celery) we keep as they are:

web: bash run-daphne.sh
channel_worker: python manage.py runworker channel_layer -v2
release: bash release.sh
beat: celery -A djangito.celery:app beat -S redbeat.RedBeatScheduler  --loglevel=DEBUG --pidfile /tmp/celerybeat.pid
worker: celery -A djangito.celery:app  worker -Q default -n djangito.%%h --without-gossip --without-mingle --without-heartbeat --loglevel=INFO --max-memory-per-child=512000 --concurrency=1

That's it. Now commit and push the code to repository on GitHub or Gitlab and let's deploy the app with Appliku.

Deploy websocket Django app to production

Go to https://app.appliku.com/startFromGitHub to create an app from your GitHub repository

Create an app: Create application Django from GitHub Repository

On the application setup screen enable two processes: web and channel_worker, then click "Reveal Config Vars".

Enable Processes from Procfile web and channel_worker

Add two config variables:

  • DJANGO_ALLOWED_HOSTS to name of your application + .applikuapp.com. For our example it is djangowebsockets.applikuapp.com
  • DJANGO_SECRET_KEY to some long string of random characters.

Add environment variables to django application

Go to "Databases tab" in the navigation.

Databases for django application

Create a postgres database on the same server as the application: Create a PostgreSQL server for django application

Create a redis server on the same server as the application: Create a Redis server for django application

Wait for "State" column for both databases become "Deployed":

Go to Settings tab and click "Reveal Config Vars" button and make see that there are two new variables DATABASE_URL and REDIS_URL. They are populated with credentials to the databases we have just created.

Database and Redis URL in environment variables for Django Application

Click "Deploy now" in the bar at the bottom of the window: Deploy Django project now

When the label says "Finished" click on the "Open App" link in the top navigation to see your app in action.

In fact, open two windows/tabs at the same time and start chatting to make sure that our channels application is actually working!

Django Channels Chat application demo

Thanks for reading!