Implement Deno WebSocket real-time applications with clustering and production deployment

Intermediate 45 min Apr 12, 2026 254 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Build production-ready real-time WebSocket applications with Deno, implementing clustering for high availability, SSL termination, and comprehensive monitoring for scalable messaging systems.

Prerequisites

  • Root or sudo access
  • Domain name for SSL certificates
  • Basic understanding of WebSockets
  • Familiarity with systemd services

What this solves

Modern applications require real-time communication for features like live chat, notifications, and collaborative editing. Deno provides excellent WebSocket support with built-in TypeScript and modern APIs, but production deployment requires clustering, load balancing, and proper monitoring. This tutorial implements a complete production-ready WebSocket infrastructure with automatic reconnection, horizontal scaling, and enterprise-grade monitoring.

Prerequisites and system preparation

Update system packages

Start by updating your package manager to ensure you get the latest versions of dependencies.

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl unzip systemd nginx certbot python3-certbot-nginx
sudo dnf update -y
sudo dnf install -y curl unzip systemd nginx certbot python3-certbot-nginx

Install Deno runtime

Download and install the latest Deno runtime with security permissions configured for production use.

curl -fsSL https://deno.land/install.sh | sh
echo 'export DENO_INSTALL="$HOME/.deno"' >> ~/.bashrc
echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
deno --version

Create application directory structure

Set up the directory structure with proper ownership for the WebSocket application and clustering configuration.

sudo mkdir -p /opt/websocket-app/{src,config,logs,static}
sudo useradd --system --shell /bin/false --home /opt/websocket-app websocket
sudo chown -R websocket:websocket /opt/websocket-app
sudo chmod 755 /opt/websocket-app
sudo chmod 775 /opt/websocket-app/logs

Step-by-step WebSocket server implementation

Create the main WebSocket server

Implement a production-ready WebSocket server with connection management, room-based messaging, and clustering support.

import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
import { serveFile } from "https://deno.land/std@0.208.0/http/file_server.ts";

interface Connection {
  socket: WebSocket;
  userId: string;
  rooms: Set;
  lastPing: number;
}

interface Message {
  type: 'join' | 'leave' | 'message' | 'ping' | 'pong';
  room?: string;
  userId?: string;
  data?: any;
  timestamp?: number;
}

class WebSocketServer {
  private connections = new Map();
  private rooms = new Map>();
  private port: number;
  private serverId: string;

  constructor(port: number) {
    this.port = port;
    this.serverId = server-${port}-${Date.now()};
    this.startPingInterval();
  }

  private startPingInterval() {
    setInterval(() => {
      const now = Date.now();
      for (const [connectionId, connection] of this.connections) {
        if (now - connection.lastPing > 30000) {
          console.log(Removing stale connection: ${connectionId});
          this.removeConnection(connectionId);
        } else {
          this.sendToConnection(connectionId, { type: 'ping', timestamp: now });
        }
      }
    }, 15000);
  }

  private generateConnectionId(): string {
    return ${this.serverId}-${crypto.randomUUID()};
  }

  private addConnection(socket: WebSocket, userId: string): string {
    const connectionId = this.generateConnectionId();
    const connection: Connection = {
      socket,
      userId,
      rooms: new Set(),
      lastPing: Date.now()
    };
    
    this.connections.set(connectionId, connection);
    console.log(New connection: ${connectionId} for user: ${userId});
    return connectionId;
  }

  private removeConnection(connectionId: string) {
    const connection = this.connections.get(connectionId);
    if (connection) {
      // Leave all rooms
      for (const room of connection.rooms) {
        this.leaveRoom(connectionId, room);
      }
      
      // Close socket if still open
      if (connection.socket.readyState === WebSocket.OPEN) {
        connection.socket.close();
      }
      
      this.connections.delete(connectionId);
      console.log(Connection removed: ${connectionId});
    }
  }

  private joinRoom(connectionId: string, room: string) {
    const connection = this.connections.get(connectionId);
    if (!connection) return;

    connection.rooms.add(room);
    
    if (!this.rooms.has(room)) {
      this.rooms.set(room, new Set());
    }
    this.rooms.get(room)!.add(connectionId);
    
    console.log(User ${connection.userId} joined room: ${room});
    
    // Notify room about new user
    this.broadcastToRoom(room, {
      type: 'message',
      userId: 'system',
      data: ${connection.userId} joined the room,
      timestamp: Date.now()
    }, connectionId);
  }

  private leaveRoom(connectionId: string, room: string) {
    const connection = this.connections.get(connectionId);
    if (!connection) return;

    connection.rooms.delete(room);
    
    const roomConnections = this.rooms.get(room);
    if (roomConnections) {
      roomConnections.delete(connectionId);
      if (roomConnections.size === 0) {
        this.rooms.delete(room);
      } else {
        // Notify room about user leaving
        this.broadcastToRoom(room, {
          type: 'message',
          userId: 'system',
          data: ${connection.userId} left the room,
          timestamp: Date.now()
        });
      }
    }
    
    console.log(User ${connection.userId} left room: ${room});
  }

  private sendToConnection(connectionId: string, message: Message) {
    const connection = this.connections.get(connectionId);
    if (connection && connection.socket.readyState === WebSocket.OPEN) {
      try {
        connection.socket.send(JSON.stringify(message));
      } catch (error) {
        console.error(Failed to send message to ${connectionId}:, error);
        this.removeConnection(connectionId);
      }
    }
  }

  private broadcastToRoom(room: string, message: Message, excludeConnectionId?: string) {
    const roomConnections = this.rooms.get(room);
    if (!roomConnections) return;

    for (const connectionId of roomConnections) {
      if (connectionId !== excludeConnectionId) {
        this.sendToConnection(connectionId, message);
      }
    }
  }

  private handleMessage(connectionId: string, message: Message) {
    const connection = this.connections.get(connectionId);
    if (!connection) return;

    connection.lastPing = Date.now();

    switch (message.type) {
      case 'join':
        if (message.room) {
          this.joinRoom(connectionId, message.room);
        }
        break;
        
      case 'leave':
        if (message.room) {
          this.leaveRoom(connectionId, message.room);
        }
        break;
        
      case 'message':
        if (message.room && message.data) {
          this.broadcastToRoom(message.room, {
            type: 'message',
            userId: connection.userId,
            data: message.data,
            timestamp: Date.now()
          });
        }
        break;
        
      case 'pong':
        // Update last ping time
        break;
    }
  }

  async start() {
    const handler = async (request: Request): Promise => {
      const { pathname } = new URL(request.url);
      
      // Serve static files
      if (pathname === '/' || pathname === '/index.html') {
        return await serveFile(request, '/opt/websocket-app/static/index.html');
      }
      
      if (pathname === '/client.js') {
        return await serveFile(request, '/opt/websocket-app/static/client.js');
      }
      
      // Health check endpoint
      if (pathname === '/health') {
        return new Response(JSON.stringify({
          status: 'healthy',
          connections: this.connections.size,
          rooms: this.rooms.size,
          serverId: this.serverId
        }), {
          headers: { 'content-type': 'application/json' }
        });
      }
      
      // WebSocket upgrade
      if (pathname === '/ws') {
        if (request.headers.get('upgrade') !== 'websocket') {
          return new Response('Expected websocket', { status: 400 });
        }
        
        const userId = new URL(request.url).searchParams.get('userId') || 'anonymous';
        const { socket, response } = Deno.upgradeWebSocket(request);
        
        const connectionId = this.addConnection(socket, userId);
        
        socket.addEventListener('message', (event) => {
          try {
            const message: Message = JSON.parse(event.data);
            this.handleMessage(connectionId, message);
          } catch (error) {
            console.error('Failed to parse message:', error);
          }
        });
        
        socket.addEventListener('close', () => {
          this.removeConnection(connectionId);
        });
        
        socket.addEventListener('error', (error) => {
          console.error('WebSocket error:', error);
          this.removeConnection(connectionId);
        });
        
        return response;
      }
      
      return new Response('Not found', { status: 404 });
    };

    console.log(WebSocket server starting on port ${this.port});
    await serve(handler, { port: this.port });
  }
}

const port = parseInt(Deno.env.get('PORT') || '8000');
const server = new WebSocketServer(port);
await server.start();

Create WebSocket client with reconnection

Build a robust client-side WebSocket implementation with automatic reconnection, exponential backoff, and connection state management.

class WebSocketClient {
  constructor(url, userId) {
    this.url = url;
    this.userId = userId;
    this.socket = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.reconnectDelay = 1000;
    this.maxReconnectDelay = 30000;
    this.isConnected = false;
    this.messageQueue = [];
    this.eventHandlers = new Map();
    
    this.connect();
  }
  
  connect() {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      return;
    }
    
    const wsUrl = ${this.url}?userId=${encodeURIComponent(this.userId)};
    console.log('Connecting to:', wsUrl);
    
    this.socket = new WebSocket(wsUrl);
    
    this.socket.addEventListener('open', (event) => {
      console.log('WebSocket connected');
      this.isConnected = true;
      this.reconnectAttempts = 0;
      this.reconnectDelay = 1000;
      
      // Send queued messages
      while (this.messageQueue.length > 0) {
        const message = this.messageQueue.shift();
        this.socket.send(JSON.stringify(message));
      }
      
      this.emit('connected', event);
    });
    
    this.socket.addEventListener('message', (event) => {
      try {
        const message = JSON.parse(event.data);
        this.handleMessage(message);
      } catch (error) {
        console.error('Failed to parse message:', error);
      }
    });
    
    this.socket.addEventListener('close', (event) => {
      console.log('WebSocket disconnected:', event.code, event.reason);
      this.isConnected = false;
      this.emit('disconnected', event);
      
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.scheduleReconnect();
      } else {
        console.error('Max reconnection attempts reached');
        this.emit('maxReconnectAttemptsReached');
      }
    });
    
    this.socket.addEventListener('error', (event) => {
      console.error('WebSocket error:', event);
      this.emit('error', event);
    });
  }
  
  scheduleReconnect() {
    this.reconnectAttempts++;
    const delay = Math.min(
      this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
      this.maxReconnectDelay
    );
    
    console.log(Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}));
    
    setTimeout(() => {
      this.connect();
    }, delay);
  }
  
  handleMessage(message) {
    switch (message.type) {
      case 'ping':
        this.send({ type: 'pong', timestamp: Date.now() });
        break;
        
      case 'message':
        this.emit('message', message);
        break;
        
      default:
        this.emit(message.type, message);
        break;
    }
  }
  
  send(message) {
    if (this.isConnected && this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify(message));
    } else {
      this.messageQueue.push(message);
    }
  }
  
  joinRoom(room) {
    this.send({ type: 'join', room });
  }
  
  leaveRoom(room) {
    this.send({ type: 'leave', room });
  }
  
  sendMessage(room, data) {
    this.send({ type: 'message', room, data });
  }
  
  on(event, handler) {
    if (!this.eventHandlers.has(event)) {
      this.eventHandlers.set(event, []);
    }
    this.eventHandlers.get(event).push(handler);
  }
  
  off(event, handler) {
    if (this.eventHandlers.has(event)) {
      const handlers = this.eventHandlers.get(event);
      const index = handlers.indexOf(handler);
      if (index > -1) {
        handlers.splice(index, 1);
      }
    }
  }
  
  emit(event, data) {
    if (this.eventHandlers.has(event)) {
      this.eventHandlers.get(event).forEach(handler => {
        try {
          handler(data);
        } catch (error) {
          console.error('Event handler error:', error);
        }
      });
    }
  }
  
  disconnect() {
    if (this.socket) {
      this.socket.close();
    }
  }
  
  getConnectionState() {
    return {
      isConnected: this.isConnected,
      reconnectAttempts: this.reconnectAttempts,
      queuedMessages: this.messageQueue.length,
      readyState: this.socket ? this.socket.readyState : WebSocket.CLOSED
    };
  }
}

Create HTML client interface

Build a simple HTML interface to test the WebSocket functionality with multiple rooms and real-time messaging.




    
    
    WebSocket Real-time Chat
    


    

WebSocket Real-time Chat

Disconnected

Room: General

Room: Tech

Configure production clustering with systemd

Create systemd service template

Set up a systemd service template that allows running multiple WebSocket server instances for horizontal scaling.

[Unit]
Description=WebSocket Server Instance %i
After=network.target
Wants=network.target

[Service]
Type=simple
User=websocket
Group=websocket
WorkingDirectory=/opt/websocket-app
Environment=PORT=%i
Environment=DENO_DIR=/opt/websocket-app/.deno
ExecStart=/home/websocket/.deno/bin/deno run --allow-net --allow-read --allow-env /opt/websocket-app/src/server.ts
Restart=always
RestartSec=5
StartLimitInterval=0
StandardOutput=journal
StandardError=journal

Security settings

NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ReadWritePaths=/opt/websocket-app/logs CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE

Resource limits

LimitNOFILE=65536 MemoryMax=1G CPUQuota=100% [Install] WantedBy=multi-user.target

Configure Deno installation for service user

Install Deno for the websocket service user and set up proper permissions for production deployment.

sudo -u websocket bash -c 'curl -fsSL https://deno.land/install.sh | sh'
sudo mkdir -p /opt/websocket-app/.deno
sudo chown websocket:websocket /opt/websocket-app/.deno

Create wrapper script for easier management

sudo tee /opt/websocket-app/start-cluster.sh << 'EOF' #!/bin/bash

Start multiple WebSocket server instances

PORTS=(8001 8002 8003 8004) for port in "${PORTS[@]}"; do echo "Starting WebSocket server on port $port" systemctl enable websocket@$port systemctl start websocket@$port done echo "WebSocket cluster started on ports: ${PORTS[*]}" EOF sudo chmod +x /opt/websocket-app/start-cluster.sh

Start WebSocket cluster instances

Deploy multiple WebSocket server instances for high availability and load distribution.

sudo systemctl daemon-reload
sudo /opt/websocket-app/start-cluster.sh

Verify all instances are running

sudo systemctl status websocket@8001 sudo systemctl status websocket@8002 sudo systemctl status websocket@8003 sudo systemctl status websocket@8004

Configure NGINX reverse proxy with load balancing

Create NGINX upstream configuration

Configure NGINX to load balance WebSocket connections across multiple Deno server instances with sticky sessions.

upstream websocket_backend {
    # Use IP hash for sticky sessions
    ip_hash;
    
    server 127.0.0.1:8001 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8002 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8003 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8004 max_fails=3 fail_timeout=30s;
}

Health check endpoint upstream

upstream websocket_health { least_conn; server 127.0.0.1:8001; server 127.0.0.1:8002; server 127.0.0.1:8003; server 127.0.0.1:8004; } map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80; server_name example.com www.example.com; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Content-Security-Policy "default-src 'self' ws: wss:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" always; # WebSocket endpoint location /ws { proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 3600s; # Prevent buffering proxy_buffering off; proxy_cache off; } # Health check endpoint location /health { proxy_pass http://websocket_health; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; access_log off; } # Static files location / { proxy_pass http://websocket_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Caching for static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { proxy_pass http://websocket_backend; proxy_cache_valid 200 1h; add_header Cache-Control "public, immutable"; } } # Security location ~ /\. { deny all; } }

Rate limiting

limit_req_zone $binary_remote_addr zone=websocket:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m; server { listen 443 ssl http2; server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; # SSL configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384; ssl_prefer_server_ciphers off; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_stapling on; ssl_stapling_verify on; # Security headers add_header Strict-Transport-Security "max-age=63072000" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; # Rate limiting limit_req zone=websocket burst=20 nodelay; # WebSocket endpoint with SSL location /ws { proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 3600s; proxy_buffering off; proxy_cache off; } # Health check location /health { proxy_pass http://websocket_health; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; access_log off; limit_req zone=api burst=10 nodelay; } # Static files and application location / { proxy_pass http://websocket_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { proxy_pass http://websocket_backend; expires 1h; add_header Cache-Control "public, immutable"; } } }

Enable NGINX configuration

Activate the NGINX configuration and obtain SSL certificates for secure WebSocket connections.

sudo ln -sf /etc/nginx/sites-available/websocket-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Obtain SSL certificate (replace example.com with your domain)

sudo certbot --nginx -d example.com -d www.example.com

Test automatic renewal

sudo certbot renew --dry-run
Note: Replace example.com with your actual domain name. For testing purposes, you can use localhost or your server's IP address, but SSL certificates require a valid domain.

Configure production monitoring and logging

Set up structured logging

Implement structured logging for better monitoring and debugging of WebSocket connections and messages.

export interface LogEntry {
  timestamp: string;
  level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG';
  message: string;
  metadata?: Record;
  serverId?: string;
}

export class Logger {
  private serverId: string;
  private logFile: string;

  constructor(serverId: string, logFile = '/opt/websocket-app/logs/app.log') {
    this.serverId = serverId;
    this.logFile = logFile;
  }

  private async writeLog(entry: LogEntry) {
    entry.serverId = this.serverId;
    entry.timestamp = new Date().toISOString();
    
    const logLine = JSON.stringify(entry) + '\n';
    
    // Write to console for systemd journal
    console.log(logLine.trim());
    
    // Write to file
    try {
      await Deno.writeTextFile(this.logFile, logLine, { append: true });
    } catch (error) {
      console.error('Failed to write to log file:', error);
    }
  }

  info(message: string, metadata?: Record) {
    this.writeLog({ level: 'INFO', message, metadata, timestamp: '', serverId: this.serverId });
  }

  warn(message: string, metadata?: Record) {
    this.writeLog({ level: 'WARN', message, metadata, timestamp: '', serverId: this.serverId });
  }

  error(message: string, metadata?: Record) {
    this.writeLog({ level: 'ERROR', message, metadata, timestamp: '', serverId: this.serverId });
  }

  debug(message: string, metadata?: Record) {
    this.writeLog({ level: 'DEBUG', message, metadata, timestamp: '', serverId: this.serverId });
  }
}

Create monitoring script

Build a monitoring script to track WebSocket server health, connection counts, and performance metrics.

#!/bin/bash

WebSocket Server Monitoring Script

LOG_FILE="/opt/websocket-app/logs/monitor.log" ALERT_EMAIL="admin@example.com" SERVICE_PORTS=(8001 8002 8003 8004) MAX_RESPONSE_TIME=5 MAX_CONNECTIONS=1000 log_message() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" } check_service_health() { local port=$1 local response_time local http_status # Check HTTP health endpoint response_time=$(curl -s -w '%{time_total}' -o /dev/null "http://localhost:$port/health" 2>/dev/null) http_status=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$port/health" 2>/dev/null) if [[ "$http_status" == "200" ]]; then if (( $(echo "$response_time > $MAX_RESPONSE_TIME" | bc -l) )); then log_message "WARNING: Port $port responding slowly ($response_time seconds)" else log_message "INFO: Port $port healthy (${response_time}s)" fi return 0 else log_message "ERROR: Port $port unhealthy (HTTP $http_status)" return 1 fi } check_systemd_service() { local port=$1 local service_name="websocket@$port" if ! systemctl is-active --quiet "$service_name"; then log_message "ERROR: Service $service_name is not running" log_message "INFO: Attempting to restart $service_name" systemctl restart "$service_name" sleep 10 if systemctl is-active --quiet "$service_name"; then log_message "INFO: Service $service_name restarted successfully" else log_message "CRITICAL: Failed to restart $service_name" send_alert "CRITICAL: WebSocket service $service_name failed to restart" fi fi } get_connection_count() { local port=$1 local health_data health_data=$(curl -s "http://localhost:$port/health" 2>/dev/null) if [[ $? -eq 0 ]]; then echo "$health_data" | grep -o '"connections":[0-9]*' | cut -d':' -f2 else echo "0" fi } check_nginx_status() { if ! systemctl is-active --quiet nginx; then log_message "ERROR: NGINX is not running" systemctl restart nginx sleep 5 if systemctl is-active --quiet nginx; then log_message "INFO: NGINX restarted successfully" else log_message "CRITICAL: Failed to restart NGINX" send_alert "CRITICAL: NGINX failed to restart" fi fi } check_disk_space() { local log_dir_usage log_dir_usage=$(df /opt/websocket-app/logs | awk 'NR==2 {print $5}' | sed 's/%//') if [[ $log_dir_usage -gt 85 ]]; then log_message "WARNING: Log directory usage at $log_dir_usage%" # Clean old logs find /opt/websocket-app/logs -name "*.log" -mtime +30 -delete fi } send_alert() { local message="$1" local subject="WebSocket Server Alert - $(hostname)" # Send email alert if mail is available if command -v mail &> /dev/null; then echo "$message" | mail -s "$subject" "$ALERT_EMAIL" fi # Log the alert log_message "ALERT: $message" } generate_metrics() { local total_connections=0 local active_services=0 for port in "${SERVICE_PORTS[@]}"; do if check_service_health "$port"; then active_services=$((active_services + 1)) connections=$(get_connection_count "$port") total_connections=$((total_connections + connections)) log_message "METRICS: Port $port - $connections connections" fi done log_message "METRICS: Total active services: $active_services/${#SERVICE_PORTS[@]}" log_message "METRICS: Total connections: $total_connections" if [[ $total_connections -gt $MAX_CONNECTIONS ]]; then send_alert "HIGH LOAD: Total connections ($total_connections) exceeds threshold ($MAX_CONNECTIONS)" fi if [[ $active_services -lt ${#SERVICE_PORTS[@]} ]]; then send_alert "SERVICE DEGRADATION: Only $active_services/${#SERVICE_PORTS[@]} services active" fi } main() { log_message "Starting WebSocket monitoring check" # Check each service for port in "${SERVICE_PORTS[@]}"; do check_systemd_service "$port" sleep 1 done # Check NGINX check_nginx_status # Check disk space check_disk_space # Generate metrics generate_metrics log_message "Monitoring check completed" }

Run main function

main

Set up automated monitoring with systemd timer

Create systemd timer to run monitoring checks every 5 minutes and log rotation for long-term maintenance.

sudo chmod +x /opt/websocket-app/monitor.sh
sudo chown websocket:websocket /opt/websocket-app/monitor.sh

Create systemd service for monitoring

sudo tee /etc/systemd/system/websocket-monitor.service << 'EOF' [Unit] Description=WebSocket Server Monitoring After=network.target [Service] Type=oneshot User=websocket Group=websocket WorkingDirectory=/opt/websocket-app ExecStart=/opt/websocket-app/monitor.sh StandardOutput=journal StandardError=journal EOF

Create systemd timer

sudo tee /etc/systemd/system/websocket-monitor.timer << 'EOF' [Unit] Description=Run WebSocket monitoring every 5 minutes Requires=websocket-monitor.service [Timer] OnCalendar=*:0/5 Persistent=true [Install] WantedBy=timers.target EOF sudo systemctl daemon-reload sudo systemctl enable websocket-monitor.timer sudo systemctl start websocket-monitor.timer sudo systemctl status websocket-monitor.timer

Configure log rotation

Set up logrotate to manage WebSocket application logs and prevent disk space issues.

/opt/websocket-app/logs/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 644 websocket websocket
    postrotate
        # Send USR1 signal to reload logs if needed
        systemctl reload websocket@8001 websocket@8002 websocket@8003 websocket@8004 || true
    endscript
}
Warning: Never use chmod 777 for log directories. The configuration above uses 644 for log files and proper ownership with the websocket user, which provides appropriate security while allowing log rotation.

Verify your setup

# Check all WebSocket services
sudo systemctl status websocket@8001 websocket@8002 websocket@8003 websocket@8004

Check NGINX status

sudo systemctl status nginx

Test health endpoints

curl -s http://localhost:8001/health | jq curl -s http://localhost:8002/health | jq

Check monitoring timer

sudo systemctl status websocket-monitor.timer

View recent logs

sudo journalctl -u websocket@8001 --since "10 minutes ago" sudo tail -f /opt/websocket-app/logs/monitor.log

Test WebSocket connection

wscat -c ws://localhost/ws?userId=testuser

Check NGINX access logs

sudo tail -f /var/log/nginx/access.log

For more advanced monitoring, you can integrate with Fluentd and Prometheus Alertmanager or set up Prometheus and Grafana monitoring for comprehensive observability.

Common issues

SymptomCauseFix
WebSocket connection failsFirewall blocking portssudo ufw allow 80,443/tcp and check NGINX config
Service won't startPort already in usesudo netstat -tlnp | grep :8001 and kill conflicting process
SSL certificate errorsDomain not pointing to serverVerify DNS records and use certbot --nginx after DNS propagation
High memory usageToo many connectionsAdjust systemd service MemoryMax and implement connection limits
Load balancing not workingNGINX upstream misconfigurationCheck sudo nginx -t and verify backend server health
WebSocket disconnects frequentlyProxy timeout too lowIncrease proxy_read_timeout in NGINX config
Permission denied on logsIncorrect ownershipsudo chown -R websocket:websocket /opt/websocket-app/logs
Monitoring script failsMissing dependenciesInstall bc and curl: sudo apt install bc curl

Next steps

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.