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
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
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
}
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
| Symptom | Cause | Fix |
|---|---|---|
| WebSocket connection fails | Firewall blocking ports | sudo ufw allow 80,443/tcp and check NGINX config |
| Service won't start | Port already in use | sudo netstat -tlnp | grep :8001 and kill conflicting process |
| SSL certificate errors | Domain not pointing to server | Verify DNS records and use certbot --nginx after DNS propagation |
| High memory usage | Too many connections | Adjust systemd service MemoryMax and implement connection limits |
| Load balancing not working | NGINX upstream misconfiguration | Check sudo nginx -t and verify backend server health |
| WebSocket disconnects frequently | Proxy timeout too low | Increase proxy_read_timeout in NGINX config |
| Permission denied on logs | Incorrect ownership | sudo chown -R websocket:websocket /opt/websocket-app/logs |
| Monitoring script fails | Missing dependencies | Install bc and curl: sudo apt install bc curl |
Next steps
- Configure NGINX reverse proxy with SSL termination and load balancing for advanced load balancing strategies
- Set up distributed tracing with OpenTelemetry and Jaeger for comprehensive observability
- Configure Deno database connections to PostgreSQL and Redis for persistent data storage
- Implement Deno JWT authentication with OAuth2 integration for secure user authentication
- Setup WebSocket horizontal autoscaling with Kubernetes for cloud-native deployments
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
DOMAIN=${1:-""}
APP_DIR="/opt/websocket-app"
SERVICE_USER="websocket"
NGINX_PORT="80"
WS_PORT_START="8080"
# Usage
usage() {
echo "Usage: $0 <domain>"
echo "Example: $0 ws.example.com"
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup on error
cleanup() {
log_error "Installation failed. Cleaning up..."
systemctl stop websocket-cluster 2>/dev/null || true
userdel -r $SERVICE_USER 2>/dev/null || true
rm -rf $APP_DIR 2>/dev/null || true
}
trap cleanup ERR
# Check arguments
[[ -z "$DOMAIN" ]] && usage
# Check if running as root
[[ $EUID -ne 0 ]] && { log_error "This script must be run as root"; exit 1; }
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
NGINX_SITES_DIR="/etc/nginx/sites-available"
NGINX_ENABLE_DIR="/etc/nginx/sites-enabled"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
NGINX_SITES_DIR="/etc/nginx/conf.d"
NGINX_ENABLE_DIR=""
FIREWALL_CMD="firewall-cmd"
# Try yum if dnf not available
if ! command -v dnf &> /dev/null; then
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
fi
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
NGINX_SITES_DIR="/etc/nginx/conf.d"
NGINX_ENABLE_DIR=""
FIREWALL_CMD="firewall-cmd"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
log_info "[1/8] Updating system packages..."
$PKG_UPDATE
log_info "[2/8] Installing dependencies..."
if [[ "$ID" =~ ^(ubuntu|debian)$ ]]; then
$PKG_INSTALL curl unzip nginx certbot python3-certbot-nginx
else
# Enable EPEL for RHEL-based systems
if [[ "$ID" =~ ^(almalinux|rocky|centos|rhel|ol)$ ]]; then
$PKG_INSTALL epel-release || true
fi
$PKG_INSTALL curl unzip nginx certbot python3-certbot-nginx
fi
log_info "[3/8] Installing Deno runtime..."
if [[ ! -f /usr/local/bin/deno ]]; then
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh
chmod 755 /usr/local/bin/deno
fi
log_info "[4/8] Creating application structure..."
mkdir -p $APP_DIR/{src,config,logs,static}
useradd --system --shell /bin/false --home $APP_DIR --no-create-home $SERVICE_USER 2>/dev/null || true
chown -R $SERVICE_USER:$SERVICE_USER $APP_DIR
chmod 755 $APP_DIR
chmod 755 $APP_DIR/{src,config,static}
chmod 775 $APP_DIR/logs
log_info "[5/8] Creating WebSocket server..."
cat > $APP_DIR/src/server.ts << 'EOF'
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
interface Connection {
socket: WebSocket;
userId: string;
rooms: Set<string>;
lastPing: number;
}
class WebSocketServer {
private connections = new Map<string, Connection>();
private rooms = new Map<string, Set<string>>();
private port: number;
constructor(port: number) {
this.port = port;
this.startPingInterval();
}
private startPingInterval() {
setInterval(() => {
const now = Date.now();
for (const [id, conn] of this.connections) {
if (now - conn.lastPing > 30000) {
this.removeConnection(id);
}
}
}, 15000);
}
async start() {
await serve(this.handleRequest.bind(this), { port: this.port });
}
private handleRequest(req: Request): Response {
if (req.headers.get("upgrade") !== "websocket") {
return new Response("WebSocket endpoint", { status: 200 });
}
const { socket, response } = Deno.upgradeWebSocket(req);
const connectionId = crypto.randomUUID();
const userId = new URL(req.url).searchParams.get("userId") || "anonymous";
const connection: Connection = {
socket,
userId,
rooms: new Set(),
lastPing: Date.now()
};
this.connections.set(connectionId, connection);
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handleMessage(connectionId, message);
} catch (e) {
console.error("Invalid message:", e);
}
};
socket.onclose = () => this.removeConnection(connectionId);
return response;
}
private handleMessage(connectionId: string, message: any) {
const connection = this.connections.get(connectionId);
if (!connection) return;
connection.lastPing = Date.now();
switch (message.type) {
case "join":
this.joinRoom(connectionId, message.room);
break;
case "leave":
this.leaveRoom(connectionId, message.room);
break;
case "message":
this.broadcastToRoom(message.room, message);
break;
}
}
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);
}
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);
}
}
}
private broadcastToRoom(room: string, message: any) {
const roomConnections = this.rooms.get(room);
if (!roomConnections) return;
for (const connectionId of roomConnections) {
const connection = this.connections.get(connectionId);
if (connection && connection.socket.readyState === WebSocket.OPEN) {
connection.socket.send(JSON.stringify(message));
}
}
}
private removeConnection(connectionId: string) {
const connection = this.connections.get(connectionId);
if (!connection) return;
for (const room of connection.rooms) {
this.leaveRoom(connectionId, room);
}
this.connections.delete(connectionId);
}
}
const port = parseInt(Deno.env.get("PORT") || "8080");
const server = new WebSocketServer(port);
console.log(`WebSocket server starting on port ${port}`);
server.start();
EOF
log_info "[6/8] Creating systemd service..."
cat > /etc/systemd/system/websocket-cluster.service << EOF
[Unit]
Description=WebSocket Cluster
After=network.target
[Service]
Type=exec
User=$SERVICE_USER
Group=$SERVICE_USER
WorkingDirectory=$APP_DIR
Environment=PORT=$WS_PORT_START
ExecStart=/usr/local/bin/deno run --allow-net --allow-env $APP_DIR/src/server.ts
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
chown root:root /etc/systemd/system/websocket-cluster.service
chmod 644 /etc/systemd/system/websocket-cluster.service
systemctl daemon-reload
systemctl enable websocket-cluster
log_info "[7/8] Configuring Nginx..."
if [[ -n "$NGINX_ENABLE_DIR" ]]; then
NGINX_CONF_FILE="$NGINX_SITES_DIR/$DOMAIN"
else
NGINX_CONF_FILE="$NGINX_SITES_DIR/$DOMAIN.conf"
fi
cat > $NGINX_CONF_FILE << EOF
upstream websocket_backend {
server 127.0.0.1:$WS_PORT_START;
}
server {
listen 80;
server_name $DOMAIN;
location / {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header 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_read_timeout 86400;
}
}
EOF
if [[ -n "$NGINX_ENABLE_DIR" ]]; then
ln -sf $NGINX_CONF_FILE $NGINX_ENABLE_DIR/
fi
nginx -t
systemctl enable nginx
systemctl restart nginx
log_info "[8/8] Configuring firewall..."
if command -v ufw &> /dev/null; then
ufw --force enable
ufw allow ssh
ufw allow 'Nginx Full'
elif command -v firewall-cmd &> /dev/null; then
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
# Start services
systemctl start websocket-cluster
log_info "Verifying installation..."
sleep 3
# Verify services
if systemctl is-active --quiet websocket-cluster; then
log_info "✓ WebSocket service is running"
else
log_error "✗ WebSocket service failed to start"
exit 1
fi
if systemctl is-active --quiet nginx; then
log_info "✓ Nginx is running"
else
log_error "✗ Nginx failed to start"
exit 1
fi
if curl -s http://localhost:$WS_PORT_START > /dev/null; then
log_info "✓ WebSocket server responding"
else
log_error "✗ WebSocket server not responding"
exit 1
fi
log_info ""
log_info "WebSocket server installation completed successfully!"
log_info "Domain: $DOMAIN"
log_info "WebSocket URL: ws://$DOMAIN/"
log_info "Service status: systemctl status websocket-cluster"
log_info "Logs: journalctl -u websocket-cluster -f"
log_info ""
log_info "To enable SSL, run: certbot --nginx -d $DOMAIN"
Review the script before running. Execute with: bash install.sh