Set up production-ready Deno WebSocket servers with authentication, clustering, and load balancing for real-time applications. Complete with systemd service configuration and NGINX reverse proxy setup.
Prerequisites
- Root or sudo access
- Basic knowledge of JavaScript/TypeScript
- Understanding of WebSocket protocol
- Familiarity with systemd services
What this solves
Real-time applications need WebSocket connections to handle bidirectional communication between clients and servers. Deno provides excellent WebSocket support, but production deployments require proper clustering, authentication, and load balancing. This tutorial shows you how to build a scalable WebSocket server with Deno that handles multiple connections, implements security middleware, and deploys with high availability.
Step-by-step installation
Install Deno runtime
Download and install the latest Deno version directly from the official repository.
curl -fsSL https://deno.land/x/install/install.sh | sh
Add Deno to your system PATH for all users.
sudo mv ~/.deno/bin/deno /usr/local/bin/
deno --version
Create WebSocket server directory structure
Set up the application directory with proper permissions for the deployment user.
sudo mkdir -p /opt/websocket-server
sudo chown $USER:$USER /opt/websocket-server
cd /opt/websocket-server
mkdir -p src middleware config logs
Build WebSocket server with connection handling
Create the main server file with WebSocket upgrade handling and broadcasting capabilities.
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
import { authenticateConnection } from "./middleware/auth.ts";
import { WebSocketManager } from "./websocket-manager.ts";
const PORT = parseInt(Deno.env.get("PORT") || "8080");
const wsManager = new WebSocketManager();
async function handler(req: Request): Promise
Implement WebSocket connection manager
Create a manager class to handle multiple connections, broadcasting, and cleanup.
interface Connection {
socket: WebSocket;
userId: string;
lastPing: number;
rooms: Set
Create authentication middleware
Implement JWT-based authentication for WebSocket connections with token validation.
import { verify } from "https://deno.land/x/djwt@v3.0.1/mod.ts";
interface AuthResult {
success: boolean;
userId?: string;
error?: string;
}
const JWT_SECRET = Deno.env.get("JWT_SECRET") || "your-secret-key-change-this";
export async function authenticateConnection(req: Request): Promise
Create environment configuration
Set up environment variables for production deployment with security settings.
PORT=8080
JWT_SECRET=your-very-secure-secret-key-change-this-in-production
NODE_ENV=production
LOG_LEVEL=info
MAX_CONNECTIONS=1000
PING_INTERVAL=30000
CONNECTION_TIMEOUT=60000
Create clustering script for load balancing
Implement a cluster manager to run multiple Deno processes for high availability.
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
const CLUSTER_SIZE = parseInt(Deno.env.get("CLUSTER_SIZE") || "4");
const BASE_PORT = parseInt(Deno.env.get("BASE_PORT") || "8080");
const workers: Deno.ChildProcess[] = [];
async function startWorker(port: number): Promise
Configure systemd service
Create a systemd service file for automatic startup and process management.
[Unit]
Description=Deno WebSocket Server
After=network.target
Wants=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/websocket-server
EnvironmentFile=/opt/websocket-server/.env
ExecStart=/usr/local/bin/deno run --allow-net --allow-env --allow-read cluster.ts
Restart=always
RestartSec=10
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30
LimitNOFILE=65536
# Security settings
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/websocket-server/logs
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=websocket-server
[Install]
WantedBy=multi-user.target
Set proper ownership and permissions for the service files.
sudo chown -R www-data:www-data /opt/websocket-server
sudo chmod 755 /opt/websocket-server
sudo chmod 644 /opt/websocket-server/.env
sudo systemctl daemon-reload
sudo systemctl enable websocket-server
Configure NGINX reverse proxy
Set up NGINX to handle WebSocket proxying with load balancing across multiple Deno processes.
sudo apt update
sudo apt install -y nginx
sudo dnf install -y nginx
Create the NGINX configuration with WebSocket support and upstream load balancing.
upstream websocket_backend {
least_conn;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8081 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8083 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name example.com www.example.com;
# Rate limiting
limit_req_zone $binary_remote_addr zone=ws_limit:10m rate=10r/s;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
location /ws {
limit_req zone=ws_limit burst=20 nodelay;
# WebSocket proxy settings
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;
# WebSocket specific timeouts
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 10s;
# Prevent proxy buffering
proxy_buffering off;
}
location /health {
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 / {
return 404;
}
}
# HTTPS configuration (add SSL certificates)
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL configuration
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
# Rate limiting
limit_req_zone $binary_remote_addr zone=wss_limit:10m rate=10r/s;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
location /ws {
limit_req zone=wss_limit burst=20 nodelay;
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 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 10s;
proxy_buffering off;
}
location /health {
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;
}
}
Enable the site and restart NGINX.
sudo ln -s /etc/nginx/sites-available/websocket-server /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Set up monitoring and logging
Create log rotation and monitoring scripts to track WebSocket connections and performance.
/opt/websocket-server/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 644 www-data www-data
postrotate
systemctl reload websocket-server
endscript
}
Create a monitoring script to check connection health.
#!/bin/bash
# WebSocket server monitoring script
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'
# Default values
DOMAIN="${1:-localhost}"
PORT="${2:-8080}"
APP_USER="websocket"
APP_DIR="/opt/websocket-server"
# Usage message
usage() {
echo "Usage: $0 [domain] [port]"
echo "Example: $0 example.com 8080"
exit 1
}
# Cleanup function for rollback
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Rolling back...${NC}"
systemctl stop websocket-server 2>/dev/null || true
systemctl disable websocket-server 2>/dev/null || true
rm -f /etc/systemd/system/websocket-server.service
userdel -r "$APP_USER" 2>/dev/null || true
rm -rf "$APP_DIR"
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
# Auto-detect distro
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
echo -e "${RED}Unsupported distro: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect OS. /etc/os-release not found${NC}"
exit 1
fi
echo -e "${GREEN}[1/8] Updating system packages...${NC}"
$PKG_UPDATE
echo -e "${GREEN}[2/8] Installing prerequisites...${NC}"
$PKG_INSTALL curl unzip
echo -e "${GREEN}[3/8] Installing Deno runtime...${NC}"
curl -fsSL https://deno.land/x/install/install.sh | DENO_INSTALL=/usr/local sh
chmod 755 /usr/local/bin/deno
echo -e "${GREEN}[4/8] Creating application user and directories...${NC}"
useradd -r -s /bin/false -d "$APP_DIR" "$APP_USER" || true
mkdir -p "$APP_DIR"/{src,middleware,config,logs}
chown -R "$APP_USER":"$APP_USER" "$APP_DIR"
chmod 755 "$APP_DIR"
chmod 755 "$APP_DIR"/{src,middleware,config}
chmod 750 "$APP_DIR/logs"
echo -e "${GREEN}[5/8] Creating WebSocket server application...${NC}"
cat > "$APP_DIR/src/main.ts" << 'EOF'
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
import { authenticateConnection } from "../middleware/auth.ts";
import { WebSocketManager } from "./websocket-manager.ts";
const PORT = parseInt(Deno.env.get("PORT") || "8080");
const wsManager = new WebSocketManager();
async function handler(req: Request): Promise<Response> {
const { pathname } = new URL(req.url);
if (pathname === "/ws") {
const upgrade = req.headers.get("upgrade") || "";
if (upgrade.toLowerCase() !== "websocket") {
return new Response("Request must be a WebSocket upgrade", { status: 426 });
}
const authResult = await authenticateConnection(req);
if (!authResult.success) {
return new Response("Unauthorized", { status: 401 });
}
const { socket, response } = Deno.upgradeWebSocket(req);
wsManager.handleConnection(socket, authResult.userId!);
return response;
}
if (pathname === "/health") {
return new Response(JSON.stringify({
status: "healthy",
connections: wsManager.getConnectionCount(),
uptime: Date.now() - startTime
}), {
headers: { "Content-Type": "application/json" }
});
}
return new Response("Not found", { status: 404 });
}
const startTime = Date.now();
console.log(`WebSocket server starting on port ${PORT}`);
serve(handler, { port: PORT });
EOF
cat > "$APP_DIR/src/websocket-manager.ts" << 'EOF'
interface Connection {
socket: WebSocket;
userId: string;
lastPing: number;
rooms: Set<string>;
}
interface Message {
type: string;
room?: string;
data: any;
userId?: string;
}
export class WebSocketManager {
private connections = new Map<string, Connection>();
private rooms = new Map<string, Set<string>>();
private pingInterval: number;
constructor() {
this.pingInterval = setInterval(() => this.pingConnections(), 30000);
}
handleConnection(socket: WebSocket, userId: string): void {
const connectionId = crypto.randomUUID();
const connection: Connection = {
socket,
userId,
lastPing: Date.now(),
rooms: new Set()
};
this.connections.set(connectionId, connection);
console.log(`User ${userId} connected (${connectionId})`);
socket.onmessage = (event) => this.handleMessage(connectionId, event);
socket.onclose = () => this.handleDisconnection(connectionId);
socket.onerror = (error) => console.error(`WebSocket error for ${connectionId}:`, error);
this.sendToConnection(connectionId, {
type: "connected",
data: { connectionId, userId }
});
}
private handleMessage(connectionId: string, event: MessageEvent): void {
try {
const message: Message = JSON.parse(event.data);
const connection = this.connections.get(connectionId);
if (!connection) return;
connection.lastPing = Date.now();
switch (message.type) {
case "join_room":
this.joinRoom(connectionId, message.room!);
break;
case "leave_room":
this.leaveRoom(connectionId, message.room!);
break;
case "broadcast":
this.broadcast(message);
break;
case "ping":
this.sendToConnection(connectionId, { type: "pong", data: {} });
break;
}
} catch (error) {
console.error("Error handling message:", error);
}
}
getConnectionCount(): number {
return this.connections.size;
}
private sendToConnection(connectionId: string, message: Message): void {
const connection = this.connections.get(connectionId);
if (connection && connection.socket.readyState === WebSocket.OPEN) {
connection.socket.send(JSON.stringify(message));
}
}
private joinRoom(connectionId: string, room: string): void {
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): void {
const connection = this.connections.get(connectionId);
if (!connection) return;
connection.rooms.delete(room);
this.rooms.get(room)?.delete(connectionId);
}
private broadcast(message: Message): void {
if (message.room) {
const roomConnections = this.rooms.get(message.room);
if (roomConnections) {
roomConnections.forEach(connId => this.sendToConnection(connId, message));
}
} else {
this.connections.forEach((_, connId) => this.sendToConnection(connId, message));
}
}
private handleDisconnection(connectionId: string): void {
const connection = this.connections.get(connectionId);
if (connection) {
connection.rooms.forEach(room => this.leaveRoom(connectionId, room));
this.connections.delete(connectionId);
console.log(`User ${connection.userId} disconnected (${connectionId})`);
}
}
private pingConnections(): void {
const now = Date.now();
const timeout = 60000;
this.connections.forEach((connection, connectionId) => {
if (now - connection.lastPing > timeout) {
console.log(`Connection ${connectionId} timed out`);
connection.socket.close();
this.handleDisconnection(connectionId);
}
});
}
}
EOF
cat > "$APP_DIR/middleware/auth.ts" << 'EOF'
export interface AuthResult {
success: boolean;
userId?: string;
error?: string;
}
export async function authenticateConnection(req: Request): Promise<AuthResult> {
const url = new URL(req.url);
const token = url.searchParams.get("token");
if (!token) {
return { success: false, error: "Missing token" };
}
// Simple token validation - replace with your auth logic
if (token.length < 10) {
return { success: false, error: "Invalid token" };
}
// Extract user ID from token (mock implementation)
const userId = `user_${token.slice(0, 8)}`;
return { success: true, userId };
}
EOF
cat > "$APP_DIR/config/server.json" << EOF
{
"port": $PORT,
"domain": "$DOMAIN",
"maxConnections": 1000,
"pingInterval": 30000,
"connectionTimeout": 60000
}
EOF
chown -R "$APP_USER":"$APP_USER" "$APP_DIR"
find "$APP_DIR" -type f -exec chmod 644 {} \;
find "$APP_DIR" -type d -exec chmod 755 {} \;
echo -e "${GREEN}[6/8] Creating systemd service...${NC}"
cat > /etc/systemd/system/websocket-server.service << EOF
[Unit]
Description=Deno WebSocket Server
After=network.target
[Service]
Type=simple
User=$APP_USER
Group=$APP_USER
WorkingDirectory=$APP_DIR
Environment=PORT=$PORT
Environment=DOMAIN=$DOMAIN
ExecStart=/usr/local/bin/deno run --allow-net --allow-read src/main.ts
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable websocket-server
echo -e "${GREEN}[7/8] Configuring firewall...${NC}"
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
ufw --force enable 2>/dev/null || true
ufw allow "$PORT"/tcp
elif [[ "$FIREWALL_CMD" == "firewall-cmd" ]]; then
systemctl enable firewalld 2>/dev/null || true
systemctl start firewalld 2>/dev/null || true
firewall-cmd --permanent --add-port="$PORT"/tcp
firewall-cmd --reload
fi
echo -e "${GREEN}[8/8] Starting WebSocket server...${NC}"
systemctl start websocket-server
sleep 3
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${YELLOW}WebSocket server is running on port $PORT${NC}"
echo -e "${YELLOW}Health check: curl http://$DOMAIN:$PORT/health${NC}"
echo -e "${YELLOW}WebSocket endpoint: ws://$DOMAIN:$PORT/ws?token=yourtoken${NC}"
echo -e "${YELLOW}Service status: systemctl status websocket-server${NC}"
echo -e "${YELLOW}Logs: journalctl -u websocket-server -f${NC}"
# Verify installation
if systemctl is-active --quiet websocket-server; then
echo -e "${GREEN}✓ Service is running${NC}"
else
echo -e "${RED}✗ Service failed to start${NC}"
exit 1
fi
if curl -s "http://localhost:$PORT/health" | grep -q "healthy"; then
echo -e "${GREEN}✓ Health check passed${NC}"
else
echo -e "${YELLOW}⚠ Health check failed (service may still be starting)${NC}"
fi
Review the script before running. Execute with: bash install.sh