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:
On the application setup screen enable two processes: web
and channel_worker
, then click "Reveal Config Vars".
Add two config variables:
DJANGO_ALLOWED_HOSTS
to name of your application +.applikuapp.com
. For our example it isdjangowebsockets.applikuapp.com
DJANGO_SECRET_KEY
to some long string of random characters.
Go to "Databases tab" in the navigation.
Create a postgres database on the same server as the application:
Create a redis server on the same server as the 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.
Click "Deploy now" in the bar at the bottom of the window:
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!
Thanks for reading!