Hello everyone,
I am encountering an issue when running my application in development mode where all WebSocket routes fail to work. This problem does not occur in production mode when using Nginx.
Below is a screenshot of the error:
The application is running with Daphne for Django Rest Framework (DRF) and Vite for React. Both the backend and frontend are running in Docker containers.
To start the application, I use the following command:
docker compose -f compose.yml -f [dev/prod].yml up --build
Django is started with the command: daphne -b 0.0.0.0 -p 8000 backend.asgi:application
Here is my code:
compose.yml:
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: backend
image: cc_guardian_backend
volumes:
- ./backend/database:/app/backend/database
networks:
- cc_guardian_net
environment:
- ENV_MODE=default
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: frontend
image: cc_guardian_frontend
networks:
- cc_guardian_net
environment:
- ENV_MODE=default
networks:
cc_guardian_net:
driver: bridge
volumes:
database:
dev.yml:
services:
backend:
environment:
- ENV_MODE=dev
ports:
- 8000:8000
command: sh /app/entrypoint.sh dev
volumes:
- ./backend:/app # Hot-reloading
frontend:
volumes:
- ./frontend:/app # Hot-reloading
- /app/node_modules # Keeps node modules
ports:
- 3000:3000
environment:
- ENV_MODE=dev
command: sh -c "./update_env.sh && npm run dev"
volumes:
backend:
frontend:
prod.yml:
services:
backend:
environment:
- ENV_MODE=prod
ports:
- 8000:8000
command: sh /app/entrypoint.sh prod
volumes:
- ./backend/static:/app/static
depends_on:
- redis
restart: unless-stopped
frontend:
volumes:
- ./frontend/dist:/app/dist # share static files
environment:
- ENV_MODE=prod
command: sh -c "./update_env.sh && npm run build"
redis:
image: "redis:alpine"
container_name: redis
networks:
- cc_guardian_net
volumes:
- ./redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf
ulimits:
nofile:
soft: 65535
hard: 65535
nginx:
build:
context: ./nginx
container_name: nginx
volumes:
- ./frontend/dist:/app/dist
- ./backend/static:/app/static
ports:
- 80:80
- 443:443
depends_on:
- frontend
- backend
networks:
- cc_guardian_net
restart: unless-stopped
redis:
restart: unless-stopped
entrypoint.sh:
#!/bin/bash
# Run migrations
python3 manage.py makemigrations --noinput
python3 manage.py migrate --noinput
if [ "$1" = "prod" ]; then
python3 manage.py collectstatic --noinput
fi
daphne -b 0.0.0.0 -p 8000 backend.asgi:application
Django settings:
INSTALLED_APPS = [
"daphne",
"channels",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_extensions",
"rest_framework",
"corsheaders",
"api",
"user_api",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
SESSION_ENGINE = (
"django.contrib.sessions.backends.db" # Store the sessions in the database
)
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "backend.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
# WSGI_APPLICATION = "backend.wsgi.application"
ASGI_APPLICATION = "backend.asgi.application"
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "database" / "db.sqlite3",
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
AUTH_USER_MODEL = "user_api.AppUser" # Custom User model
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static")
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
Dev settings.py:
CORS_ALLOW_CREDENTIALS = True
DEBUG = True
ALLOWED_HOSTS = ["*"]
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
},
}
CSRF_TRUSTED_ORIGINS = [
"http://127.0.0.1:3000",
"http://127.0.0.1:8000",
"http://localhost:3000",
"http://localhost:8000",
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:8000",
"http://127.0.0.1:8000",
]
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
consumers.py
class ServerUpdateStatusConsumer(WebsocketConsumer):
def connect(self):
print(f"WebSocket connect: {self.scope['client']}")
self.server_hostname = self.scope["url_route"]["kwargs"].get(
"server_hostname", None
)
if self.server_hostname:
# Specific group to each server (send to environment page)
self.room_group_name = f"server_update_{self.server_hostname}"
else:
# General group for all the servers (send to dashboard page)
self.room_group_name = "server_update_general"
# Add the consumer to the group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name, self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Remove the consumer when disconnect
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name, self.channel_name
)
def receive(self, text_data):
# Can be used for frontend response
pass
# This method handles the server_status_update message
def server_status_update(self, event):
# Send the data to the WebSocket
self.send(
text_data=json.dumps(
{
"hostname": event["hostname"],
"key": event["key"],
"status": event["status"],
"child_hostname": event.get(
"child_hostname", None
), # Include child_hostname if available
}
)
)
asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from .routing import websocket_urlpatterns
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
),
}
)
routing.py:
from django.urls import re_path
from .consumers import LogsConsumer, ServerUpdateStatusConsumer
websocket_urlpatterns = [
re_path(r"ws/logs/(?P<server_hostname>[\w-]+)/$", LogsConsumer.as_asgi()),
re_path(r"ws/server-update-status/$", ServerUpdateStatusConsumer.as_asgi()),
re_path(
r"ws/server-update-status/(?P<server_hostname>[\w-]+)/$",
ServerUpdateStatusConsumer.as_asgi(),
),
]
Javascript Code:
import React, { useEffect, useState } from 'react';
import api from '../api';
import Card from '../components/Card';
import { Box, Grid, Typography, CircularProgress } from '@mui/material';
const TruncatedTitle = (title) => {
return title.length > 49 ? `${title.slice(0, 46)}...` : title;
};
const Dashboard = () => {
const [servers, setServers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const wsUrl = import.meta.env.VITE_API_BASE_URL_WS;
// Load server data
useEffect(() => {
const fetchServers = async () => {
try {
const response = await api.get(`/api/user-linked-servers/`);
setServers(response.data);
} catch (error) {
console.error('Error searching servers:', error);
} finally {
setIsLoading(false);
}
};
fetchServers();
const ws = new WebSocket(`${wsUrl}/ws/server-update-status/`);
ws.onopen = (event) => {
console.log('WebSocket connection established');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = (event) => {
console.log('WebSocket connection closed:', event);
};
ws.onmessage = (event) => {
try {
const updatedData = JSON.parse(event.data);
console.log('Received WebSocket message:', data);
const { hostname, key, status, child_hostname } = updatedData;
setServers((prevServers) =>
prevServers.map((server) => {
if (server.hostname === hostname) {
switch (key) {
case 'application_status':
case 'queues_status':
case 'event_viewer_status':
case 'services_status':
return {
...server,
[key]: { status: status }
};
case 'kafka':
case 'databases':
case 'sbs':
case 'nms':
case 'collectors':
return {
...server,
[key]: server[key].map((item) => {
return item.hostname.toLowerCase() ===
child_hostname.toLowerCase()
? { ...item, status: status }
: item;
})
};
default:
return server;
}
}
return server;
})
);
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
return () => {
ws.close();
};
}, []);
api.js
import axios from 'axios';
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(
cookie.substring(name.length + 1)
);
break;
}
}
}
return cookieValue;
}
const csrfToken = getCookie('csrftoken');
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
axios.defaults.withCredentials = true;
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8000',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json',
Accept: 'application/json'
}
});
export default api;
vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
server: {
host: '0.0.0.0',
port: 3000,
},
plugins: [react()],
});