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
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.
<h1>WebSocket Real-time Chat</h1>
<div class="user-controls">
</div>
<div class="status disconnected">Disconnected</div>
<div class="container">
<div class="chat-room">
<h3>Room: General</h3>
<div class="messages"></div>
<div class="message-input">
</div>
<div>
</div>
</div>
<div class="chat-room">
<h3>Room: Tech</h3>
<div class="messages"></div>
<div class="message-input">
</div>
<div>
</div>
</div>
</div>
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