Search code examples
pythondjangodockerdjango-channels

websockets with Django channels rest framework in docker-compose


I've made an application with two websockets connections using Django channels rest framework. Locally, everything works fine. I have such settings:

#routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from django.urls import re_path
from gnss.consumers import CoordinateConsumer  # noqa
from operations.consumers import OperationConsumer  # noqa

websocket_urlpatterns = [
    re_path(r'ws/coordinates/', CoordinateConsumer.as_asgi()),
    re_path(r'ws/operations/', OperationConsumer.as_asgi()),
]

application = ProtocolTypeRouter({
    'http': get_asgi_application(),
    'websocket': AuthMiddlewareStack(
        URLRouter(websocket_urlpatterns)
    ),
})

# consumers.py
from djangochannelsrestframework.decorators import action  # noqa
from djangochannelsrestframework.generics import GenericAsyncAPIConsumer  # noqa
from djangochannelsrestframework.observer import model_observer  # noqa
from djangochannelsrestframework.observer.generics import action  # noqa
from djangochannelsrestframework.permissions import AllowAny  # noqa

from .models import Coordinate
from .serializers import CoordinateSerializer


class CoordinateConsumer(GenericAsyncAPIConsumer):
    queryset = Coordinate.objects.all()
    serializer_class = CoordinateSerializer
    permission_classes = (AllowAny,)

    @model_observer(Coordinate)
    async def coordinates_activity(self, message, action=None, **kwargs):
        await self.send_json(message)

    @coordinates_activity.serializer
    def coordinates_activity(self, instance: Coordinate, action, **kwargs):
        return dict(CoordinateSerializer(instance).data,
                    action=action.value, pk=instance.pk)

    @action()
    async def subscribe_to_coordinates_activity(self, request_id, **kwargs):
        await self.coordinates_activity.subscribe(request_id=request_id)

As for the consumers.py for the ws/operations/ it's exactly the same. When I run my Django app on local machine in development server (python manage.py runserver) everything works fine. But for some reason, when I run my app in docker-compose, ws/operations/ works, but ws/coordinates not. I don't recieve messages from Django for some reason.

My docker-compose

version: "3.7"

x-api-common: &api
  build: ./backend
  restart: always
  env_file: ./config/api/.env
  volumes:
    - ./backend/api:/opt/code
    - static_volume:/home/app/web/staticfiles
    - media_volume:/home/app/web/media
  networks:
    - nginx_network
    - postgres_network
  depends_on:
    - redis
    - postgres

services:

  wsgi:
    <<: *api
    command: "bash /opt/code/docker-wsgi-entrypoint.sh"
    container_name: operations_wsgi
    environment:
      is_wsgi: 'true'
    ports:
      - 8000:8000

  asgi:
    <<: *api
    command: "bash /opt/code/docker-asgi-entrypoint.sh"
    container_name: operations_asgi
    ports:
      - 8001:8001
    depends_on:
      - wsgi

  admin:
    <<: *api
    container_name: operations_admin
    ports:
      - 8002:8002
    depends_on:
      - wsgi
    command: gunicorn api.wsgi:application --bind 0.0.0.0:8002 --workers 2 --timeout 600

  web:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    restart: "on-failure"
    ports:
      - 80:80
    volumes:
      - ./frontend:/app
      - '/app/node_modules'
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/media
    depends_on:
      - wsgi
      - asgi
      - admin
    networks:
      - nginx_network

  redis:
    image: "redis:6.2.7-alpine"
    restart: always
    command:
      - /bin/sh
      - -c
      - redis-server --requirepass "$${REDIS_PASSWORD:?REDIS_PASSWORD variable is not set}"
    ports:
      - "6379:6379"
    env_file:
      - ./config/redis/.env
    networks:
      - nginx_network

  postgres:
    image: postgres:13.0
    restart: always
    ports:
      - 5432:5432
    env_file: config/postgres/.env
    networks:
      - postgres_network
    volumes:
      - ./data/postgres-data:/var/lib/postgresql/data

  rabbitmq:
    image: 'rabbitmq:3.6-management-alpine'
    restart: always
    ports:
      - '5672:5672'
      - '15672:15672'
    env_file: config/rabbitmq/.env

networks:
  nginx_network:
    driver: bridge
  postgres_network:
    driver: bridge

volumes:
  static_volume:
  media_volume:

And my nginx config

upstream wsgi_server {
    server wsgi:8000;
}

upstream asgi_server {
    server asgi:8001;
}

upstream admin_server {
    server admin:8002;
}

server {

  listen 80;
  server_name localhost;
  charset utf-8;
  server_tokens off;
  client_max_body_size 20M;
  resolver 10.0.0.2 valid=300s;
  resolver_timeout 10s;

    location ~* ^/ws/ {
      proxy_pass http://asgi_server;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "Upgrade";
      proxy_set_header Host            $host;
      proxy_set_header X-Forwarded-For $remote_addr;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Scheme $scheme;
      proxy_set_header REMOTE_ADDR $remote_addr;
    }


    location ~ ^/static/(?<base_path>rest_framework|admin|drf-yasg)/(?<tail>.*)$ {
        autoindex on;
        alias /home/app/web/staticfiles/$base_path/$tail;
    }

    location ~ ^/media/(?<base_path>images)/(?<tail>.*)$ {
        autoindex on;
        alias /home/app/web/media/$base_path/$tail;
        add_header Content-disposition "attachment";
    }

    location ~ ^/api|swagger|redoc/ {
        proxy_pass http://wsgi_server;
        proxy_pass_request_headers on;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
    }

    location ~ ^/admin/ {
        proxy_pass http://admin_server;
        proxy_pass_request_headers on;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
    }

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }
}

I don't know what's wrong here


Solution

  • To solve the issue:

    1. Add def ready() function to apps.py like that:
    from django.apps import AppConfig
    
    class GnssConfig(AppConfig):
        name = 'gnss'
        verbose_name = 'GNSS management'
    
        def ready(self) -> None:
            from .consumers import CoordinateConsumer  # noqa
    
    1. Configure nginx like that:
        location ~ ^/ws/operations|coordinates/ {
          proxy_pass http://asgi_server;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "upgrade";
        }
    

    This is an example for two endpooints ws/operations and ws/coordinates