Django Daphne not working with WSS, Ngnix and Docker

I'm trying to deploy an application built with Django and Daphne to open a WebSocket connection. Everything works perfectly on localhost, as I'm not using Nginx.

However, in production, I need to use Nginx to serve SSL/HTTPS connections, including WSS. Unfortunately, it no longer works. When I try to use HTTPS on port 443, the WebSocket connection remains pending indefinitely until it eventually fails.

I really considered removing Nginx because I found a Daphne configuration that runs on port 443 with an SSL certificate.

daphne -b 0.0.0.0 -e ssl:443:privateKey=/privkey.pem:certKey=/fullchain.pem core.asgi:application

However, I would still need Nginx to serve static files.

I feel like I've tried everything so far. Can someone help me?

docker-compose.yml

services:
  web:
    build: .
    container_name: meetlink_django_app
    ports:
      - 8000:8000
      - 8001:8001

    volumes:
      - .:/app
      - static_volume:/app/core/staticfiles
      - ./certbot/conf:/app/certbot/conf:ro
      - ./certbot/www:/app/certbot/www:ro

    environment:
      - DEBUG=True
      - PYTHONUNBUFFERED=1
      - DJANGO_SETTINGS_MODULE=core.settings
      - ENV=${ENV}

    networks:
      - meetlink-network

    command:
      - bash
      - -c
      - "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 & daphne -b 0.0.0.0 -p 8001 core.asgi:application"

    depends_on:
      database:
        condition: service_healthy
      cache:
        condition: service_healthy

  database:
    image: postgres
    container_name: meetlink_django_database
    environment:
      - POSTGRES_USER=${DB_USERNAME}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}

    ports:
      - ${DB_PORT}:5432

    volumes:
      - database_volume:/var/lib/postgresql/data

    networks:
      - meetlink-network

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
      interval: 5s
      timeout: 10s
      retries: 5

  cache:
    image: redis
    container_name: meetlink_django_cache

    ports:
      - ${CACHE_PORT}:6379

    networks:
      - meetlink-network

    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 10s
      retries: 5

  server:
    image: nginx
    container_name: meetlink_django_server

    ports:
      - ${SSL_SERVER_PORT}:443
      - ${SERVER_PORT}:80

    depends_on:
      - web

    networks:
      - meetlink-network

    volumes:
      - ./.nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - static_volume:/var/www/html/static
      - ./certbot/www/:/var/www/certbot:ro
      - ./certbot/conf/:/var/www/conf:ro

  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./certbot/www/:/var/www/certbot/:rw
      - ./certbot/conf/:/etc/letsencrypt/:rw

    network_mode: host

volumes:
  static_volume:
  database_volume:

networks:
  meetlink-network:
    driver: bridge

.nginx/default.conf

server {
    listen 80;
    listen [::]:80;

    server_name totemvirtual.com.br www.totemvirtual.com.br;

    tcp_nodelay on;

    location / {
        proxy_pass http://meetlink_django_app:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_buffering off;
    }

    location /ws/ {
        proxy_pass http://meetlink_django_app:8001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_buffering off;
    }

    location /static/ {
        autoindex on;
        alias /var/www/html/static/;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name totemvirtual.com.br www.totemvirtual.com.br;

    ssl_certificate /var/www/conf/live/totemvirtual.com.br/fullchain.pem;
    ssl_certificate_key /var/www/conf/live/totemvirtual.com.br/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_session_tickets off;  

    proxy_connect_timeout 43200000;

    tcp_nodelay on;

    location / {
        proxy_pass http://meetlink_django_app:8001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_buffering off;
    }

    location /static/ {
        autoindex on;
        alias /var/www/html/static/;
    }
}

core/asgi.py

import os

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 meetlink.domain.call.call_consumer import CallConsumer
from meetlink.domain.meeting.meeting_consumer import MeetingConsumer

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

application = ProtocolTypeRouter(
    {
        "http": get_asgi_application(),
        "websocket": AuthMiddlewareStack(
            URLRouter(
                [
                    re_path("ws/calls/?$", CallConsumer.as_asgi()),
                    re_path("ws/meetings/?$", MeetingConsumer.as_asgi()),
                ]
            )
        ),
    }
)

core/settings.py | channel layers config

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": config("CHANNELS_CACHE_ENGINE"),
        "CONFIG": {
            "hosts": [(config("CACHE_HOST"), config("CACHE_PORT"))],
            "prefix": "meetlink.channels",
        },
    },
}

Dockerfile (I think it's make no difference)

FROM python:3.13-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    build-essential \
    python3-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN pip install --no-cache-dir pipenv

COPY Pipfile Pipfile.lock /app/

RUN pipenv install --system --deploy

COPY . /app/

WORKDIR /app/core/

COPY start.sh /app/start.sh

RUN python manage.py collectstatic --noinput --clear

EXPOSE 8000 8001 443 80
Вернуться на верх