DEV Community

Cover image for Celery | Redis | WebSocket in Django with Docker
TheHormat
TheHormat

Posted on

Celery | Redis | WebSocket in Django with Docker

I’m here with a great topic. I’ve gathered together some amazing tools that many beginners or those who have never used them are very curious about but have a hard time using them. In this article, I’m going to use Celery, Redis and WebSocket in a very basic task in Django and I’m going to tell you how they work and how to set them up.

Since the topic is already long, I don’t think I need to go into a lot of fine details. If you are already interested in these topics, I think you don’t need fine details. Let’s start directly with Celery and Redis and complete their installation.

📌 Let me first explain our simple task and what we will learn by doing it. We will have a Student model. Using Celery and Redis we will try to delete an object of this model every 10 seconds. Then what we will do with WebSocket is to display these events live in an html page. This way we will see every data that is added or deleted on our site without refreshing the page.

Image description

> CELERY & REDIS

1️⃣ First let’s download Celery and Redis:

pip install celery redis

If Redis is not downloaded on your computer you will get an error, so download Redis first:

brew install redis

2️⃣ Now it’s time to wake the sleeping giant:

redis-server

Other commands you will run:

celery -A myproject worker --loglevel=info
celery -A myproject beat --loglevel=info
Enter fullscreen mode Exit fullscreen mode

🔊 At first you may find it tiring and pointless to work with a few terminals, but don’t worry, we’ll take care of that with Docker. In the next steps we learn how to install these tools separately.

3️⃣ Now we will create the necessary files and write our code in them:

# home/models.py:
from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=50)
    surname = models.CharField(max_length=50)
    age = models.IntegerField()

    def __str__(self):
        return self.name


# core/celery.py
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')

app = Celery('core')
app.config_from_object('django.conf:settings', namespace='CELERY')

app.autodiscover_tasks(['core'])

@app.task(bind=True)
def debug_task(self):
    print(f'Request: {self.request!r}')


# core/__init__.py
from __future__ import absolute_import, unicode_literals
from .celery import app as celery_app

__all__ = ('celery_app',)


# core/settings.py:
CELERY_BROKER_URL = "redis://redis:6379/0"
CELERY_RESULT_BACKEND = "redis://redis:6379/0"
CELERY_BEAT_SCHEDULE = {
    "delete-student-objects-every-10-seconds": {
        "task": "core.tasks.delete_student_objects",
        "schedule": 10.0,  # Every 10 seconds
    },
}

CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True


# core/tasks.py
from celery import shared_task
from home.models import Student

@shared_task
def delete_student_objects():
    # Find and delete the most recently added student object
    latest_student = Student.objects.order_by('-id').first()
    if latest_student:
        latest_student.delete()
        print(f"Deleted student: {latest_student.name} {latest_student.surname}")
    else:
        print("No students to delete.")
Enter fullscreen mode Exit fullscreen mode

Okay, now objects in your model will be dynamically deleted every 10 seconds. We’re done with Celery and Redis. Now let’s use WebSocket to see these changes live on our site.

> WEBSOCKET | CHANNELS

️pip install channels channels-redis

After downloading, let’s move on to the code. The code side may be a bit long but it’s worth it for the look at the end:

# core/settings.py
INSTALLED_APPS = [
    # Other apps
    'channels',
    'home',  # Student model in app
]

ASGI_APPLICATION = 'core.asgi.application'

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("redis", 6379)],
        },
    },
}



# core/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import home.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter(
            home.routing.websocket_urlpatterns
        )
    ),
})



# core/routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack
import home.routing

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter(
            home.routing.websocket_urlpatterns
        )
    ),
})



# home/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

class StudentConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.channel_layer.group_add(
            "students",
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(
            "students",
            self.channel_name
        )

    async def receive(self, text_data):
        data = json.loads(text_data)
        message = data['message']

        await self.channel_layer.group_send(
            "students",
            {
                'type': 'student_message',
                'message': message
            }
        )

    async def student_message(self, event):
        message = event['message']

        await self.send(text_data=json.dumps({
            'message': message
        }))



# home/routing.py
from django.urls import path
from . import consumers

websocket_urlpatterns = [
    path('ws/students/', consumers.StudentConsumer.as_asgi()),
]



# home/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import Student
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

@receiver(post_save, sender=Student)
def student_saved(sender, instance, **kwargs):
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        "students",
        {
            'type': 'student_message',
            'message': f'Student {instance.name} {instance.surname} has been added.'
        }
    )

@receiver(post_delete, sender=Student)
def student_deleted(sender, instance, **kwargs):
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        "students",
        {
            'type': 'student_message',
            'message': f'Student {instance.name} {instance.surname} has been deleted.'
        }
    )




# home/apps.py
from django.apps import AppConfig

class HomeConfig(AppConfig):
    name = 'home'

    def ready(self):
        import home.signals



# home/views.py
from django.shortcuts import render
from .models import Student

def homePage(request):
    students = Student.objects.all()
    context = {"students": students}
    return render(request, 'index.html', context=context)
Enter fullscreen mode Exit fullscreen mode

Yes, after a long coding, we have set up our WebSocket for python. Now let’s write our code in our html file and run our django server to see the result. Note that celery and redis should remain running during this process.

<!-- templates/index.html-->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>The Magnificent Four</title>
</head>

<body>
    <h2 style="text-align: center;">Celery | Redis | WebSocket | Docker</h2>

    <div class="students">
        <ul id="students-list">
            {% for student in students %}
                <li id="student-{{ student.id }}">Student: {{ student.name }} {{ student.surname }} - Age: {{ student.age }}
            </li>
            {% endfor %}
        </ul>
        <p id="no-students" {% if students %} style="display:none;" {% endif %}>No students available</p>
    </div>

    <script>
        const studentList = document.getElementById('students-list');
        const noStudentsMessage = document.getElementById('no-students');

        const ws = new WebSocket('ws://' + window.location.host + '/ws/students/');

        ws.onmessage = function (event) {
            const data = JSON.parse(event.data);
            const message = data.message;

            if (message.includes('added')) {
                const li = document.createElement('li');
                li.textContent = message;
                studentList.appendChild(li);
                noStudentsMessage.style.display = 'none';
            } else if (message.includes('deleted')) {
                const li = document.querySelector(`#students-list li:last-child`);
                if (li) {
                    li.remove();
                }
                if (!studentList.children.length) {
                    noStudentsMessage.style.display = 'block';
                }
            }
        };
    </script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Finally, we are done, after the “python3 manage.py runserver” command, you will see that the database in list format at the url “http://127.0.0.1:8000/" is both added and deleted after 10 seconds. I think this is something you like. We turned our Django into a dragon :D.

I can hear you saying, “Hey, man, where are you going, you can’t leave us alone with all these terminals.” No need to panic, let’s take care of that and increase our pleasure.

> DOCKER

1️⃣ First you will need to download the Docker. After downloading, join Docker. After making the necessary settings, it will already work every time you enter the Docker.

2️⃣ Create Dockerfile and docker-compose.yml:

# Dockerfile
FROM python:3.12-slim

WORKDIR /code

RUN pip install --no-cache-dir poetry

RUN poetry config virtualenvs.create false

COPY . /code/

RUN poetry install --no-root --no-dev --no-interaction --no-ansi

EXPOSE 8000

CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "core.asgi:application"]



# docker-compose.yml
services:
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

  django:
    build: .
    command: daphne -b 0.0.0.0 -p 8000 core.asgi:application
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    depends_on:
      - redis
      - celery_worker
      - celery_beat

  celery_worker:
    build: .
    command: celery -A core worker --loglevel=info
    volumes:
      - .:/code
    depends_on:
      - redis

  celery_beat:
    build: .
    command: celery -A core beat --loglevel=info
    volumes:
      - .:/code
    depends_on:
      - redis
Enter fullscreen mode Exit fullscreen mode

That’s it, it’s finally over, yeah. I guess I’m as tired as you are. But I think it was worth it. I hope you do too. Now you just need to type “docker compose up — build” in a terminal and run it. Then both Celery, Redis and WebSocket will be up and running.

I hope this blog has been useful for you. In this blog, we looked at how to get the necessary tools working without getting too deep into the subject. Of course, it would be great if you researched the code and, as I said, prepared in advance to learn about Celery, Redis, WebSocket, Docker, etc.

Conclusion
You don’t have to panic about it, you can easily find it and fix it after a detailed search. At most, you will get an error about which versions of the tools are incompatible.

> Find the full scope of codes here. Star⭐ the GitHub repository.

Before you go… If you have any questions/suggestions/thoughts, do drop me a line below. 🖋️

And if you enjoyed this, let us know how you felt with a nice emoji(🤯❤️‍🔥) and don't forget to follow for future updates.

That’s it from me. We will talk soon!

— TheHormat ♟️

Top comments (0)