Back to Blog
Tutorial11 min read2026-06-28

How to Deploy Django Channels for WebSockets

Real-time Django needs ASGI, not WSGI. Here's how to deploy Django Channels with Daphne or Uvicorn, a Redis channel layer, sticky connections, and a managed Postgres.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Real-time Django is a different deploy

A standard Django app runs under WSGI (Gunicorn) and handles request/response. The moment you need WebSockets — live chat, notifications, collaborative editing — you need ASGI and Django Channels. That changes your server, adds a Redis channel layer for cross-process messaging, and introduces connection-affinity concerns. Let's deploy it correctly.

Step 1: ASGI application and routing

Channels wraps your Django app in an ASGI application that routes HTTP to Django and WebSocket to consumers:

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

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
django_asgi = get_asgi_application()

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

Step 2: A consumer

# chat/consumers.py
from channels.generic.websocket import AsyncJsonWebsocketConsumer

class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.room = self.scope["url_route"]["kwargs"]["room"]
        await self.channel_layer.group_add(self.room, self.channel_name)
        await self.accept()

    async def receive_json(self, content):
        await self.channel_layer.group_send(
            self.room, {"type": "chat.message", "text": content["text"]})

    async def chat_message(self, event):
        await self.send_json({"text": event["text"]})

Step 3: The Redis channel layer

Groups and cross-process messaging require a channel layer backed by Redis. Provision a managed Redis on PandaStack and point Channels at it:

# settings.py
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {"hosts": [os.environ["REDIS_URL"]]},
    },
}

Without Redis, a multi-replica deploy can't broadcast across instances — a message sent to a user on replica A never reaches a user on replica B. The channel layer is what makes horizontal scaling work.

Step 4: Run an ASGI server

Use Daphne (Channels' reference server) or Uvicorn. Bind to the injected port:

# Daphne
daphne -b 0.0.0.0 -p $PORT config.asgi:application

# or Uvicorn
uvicorn config.asgi:application --host 0.0.0.0 --port $PORT
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD daphne -b 0.0.0.0 -p $PORT config.asgi:application

Step 5: Wire the managed Postgres

Django's ORM still uses Postgres for persistent data. PandaStack injects DATABASE_URL when you attach a managed PostgreSQL; parse it with dj-database-url:

import dj_database_url
DATABASES = {"default": dj_database_url.config(default=os.environ["DATABASE_URL"])}

Run migrations as a release step: python manage.py migrate.

Step 6: Connection affinity matters

WebSocket connections are long-lived. A few deployment realities:

  • Ingress must support WebSocket upgrade. PandaStack uses Kong ingress, which handles the HTTP upgrade to ws:///wss:// — your wss:// connections over the custom domain get automatic SSL.
  • Scale horizontally with the Redis layer, not by relying on a single big instance. The channel layer broadcasts across replicas.
  • Avoid scale-to-zero for realtime. Free-tier apps scale to zero on spot nodes, which will drop idle WebSocket connections and cold-start. For production realtime, use a paid tier that stays warm.

Step 7: Deploy

Connect your repo and push:

git push origin main

The build runs in a rootless BuildKit K8s Job pod, ships to Artifact Registry, and Helm-deploys. Watch the WebSocket handshakes in live logs, and confirm wss://yourdomain/ws/room/ upgrades cleanly.

Step 8: Static files and the HTTP side

Channels still serves your normal Django views over HTTP. Use WhiteNoise or object storage for static files (don't rely on container disk), and collectstatic at build time.

Production checklist

  • [ ] ASGI server (Daphne/Uvicorn) bound to $PORT
  • [ ] Redis channel layer for cross-replica broadcast
  • [ ] DATABASE_URL injected; migrations as release step
  • [ ] Ingress passes WebSocket upgrade (Kong does)
  • [ ] Paid/warm tier for realtime (no scale-to-zero)
  • [ ] Static files via WhiteNoise or object storage

References

  • [Django Channels — Deploying](https://channels.readthedocs.io/en/latest/deploying.html)
  • [channels-redis](https://github.com/django/channels_redis)
  • [Daphne](https://github.com/django/daphne)
  • [Django — ASGI deployment](https://docs.djangoproject.com/en/stable/howto/deployment/asgi/)

---

Realtime Django works beautifully on containers once you've got ASGI plus a Redis channel layer and WebSocket-aware ingress. Deploy Channels on PandaStack with a managed Postgres and Redis — start on the [free tier](https://dashboard.pandastack.io) to prototype, then move to a warm paid tier for production connections.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also