WebSocket com Node.js na VPS

    WebSocket permite comunicação bidirecional em tempo real entre servidor e cliente — essencial para chats, dashboards ao vivo, notificações push e jogos multiplayer. Na VPS, o Nginx precisa de configuração específica para fazer proxy de conexões WebSocket, e aplicações com múltiplos workers exigem um adapter Redis para sincronizar eventos entre processos.

    Socket.IO: servidor WebSocket com fallback

    Socket.IO é a biblioteca mais popular — inclui fallback para long-polling quando WebSocket não está disponível e suporte nativo a salas:

    typescript
    // src/index.ts
    import express from 'express'
    import { createServer } from 'http'
    import { Server } from 'socket.io'
    
    const app = express()
    const httpServer = createServer(app)
    const io = new Server(httpServer, {
      cors: {
        origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
        methods: ['GET', 'POST'],
      },
      // Configurações para produção
      pingTimeout: 60000,
      pingInterval: 25000,
      transports: ['websocket', 'polling'],
    })
    
    io.on('connection', (socket) => {
      console.log('Cliente conectado:', socket.id)
    
      socket.on('join-room', (roomId: string) => {
        socket.join(roomId)
        socket.to(roomId).emit('user-joined', { userId: socket.id })
      })
    
      socket.on('message', (data: { room: string; text: string }) => {
        io.to(data.room).emit('message', {
          from: socket.id,
          text: data.text,
          timestamp: new Date().toISOString(),
        })
      })
    
      socket.on('disconnect', (reason) => {
        console.log('Cliente desconectado:', socket.id, reason)
      })
    })
    
    httpServer.listen(3000)

    Nginx: proxy para WebSocket

    O Nginx precisa dos headers Upgrade e Connection para fazer proxy de WebSocket. Sem eles, a conexão cai silenciosamente para HTTP:

    nginx
    # nginx.conf para WebSocket
    server {
        listen 443 ssl;
        http2 on;
        server_name ws.seudominio.com.br;
    
        ssl_certificate /etc/letsencrypt/live/ws.seudominio.com.br/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/ws.seudominio.com.br/privkey.pem;
    
        location / {
            proxy_pass http://app:3000;
            proxy_http_version 1.1;
    
            # Headers obrigatórios para WebSocket
            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;
    
            # Timeouts para conexões de longa duração
            proxy_read_timeout 3600s;   # 1 hora sem mensagem
            proxy_send_timeout 3600s;
            proxy_connect_timeout 10s;
        }
    }
    Dica
    proxy_read_timeout 3600s permite conexões WebSocket ativas por até 1 hora sem tráfego antes do Nginx fechar. Ajuste conforme o comportamento esperado dos seus clientes.

    Redis Adapter: WebSocket com múltiplos workers

    Com PM2 cluster ou múltiplos containers, cada worker tem sua própria instância Socket.IO. Um evento emitido no worker 1 não chega aos clientes do worker 2. Redis adapter sincroniza eventos entre workers:

    typescript
    // src/socket.ts
    import { Server } from 'socket.io'
    import { createAdapter } from '@socket.io/redis-adapter'
    import { createClient } from 'redis'
    
    export async function setupSocketIO(httpServer: any) {
      const io = new Server(httpServer)
    
      // Criar dois clientes Redis: pub e sub
      const pubClient = createClient({ url: process.env.REDIS_URL })
      const subClient = pubClient.duplicate()
    
      await Promise.all([pubClient.connect(), subClient.connect()])
    
      // Configurar adapter Redis
      io.adapter(createAdapter(pubClient, subClient))
    
      io.on('connection', (socket) => {
        // Agora eventos são sincronizados entre todos os workers
        socket.on('message', (data) => {
          // io.to(room).emit() propaga para clientes em TODOS os workers
          io.to(data.room).emit('message', data)
        })
      })
    
      return io
    }
    Dica
    Sem o Redis adapter, aplicações com múltiplos workers (PM2 cluster ou múltiplos containers) têm comportamento imprevisível: clientes conectados a workers diferentes não se comunicam.

    Autenticação em WebSocket

    WebSocket não tem headers HTTP após o handshake inicial — use token JWT passado na conexão:

    typescript
    // Middleware de autenticação Socket.IO
    import jwt from 'jsonwebtoken'
    
    io.use((socket, next) => {
      const token = socket.handshake.auth.token
                   || socket.handshake.query.token as string
    
      if (!token) {
        return next(new Error('Token não fornecido'))
      }
    
      try {
        const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload
        socket.data.userId = payload.sub
        next()
      } catch (err) {
        next(new Error('Token inválido ou expirado'))
      }
    })
    
    // No cliente JavaScript:
    const socket = io('wss://ws.seudominio.com.br', {
      auth: { token: localStorage.getItem('access_token') },
    })

    ws: biblioteca WebSocket nativa (sem Socket.IO)

    Para casos simples sem necessidade de salas ou fallback, a biblioteca ws é mais leve que Socket.IO:

    typescript
    // Servidor WebSocket com ws (mais leve que Socket.IO)
    import { WebSocketServer, WebSocket } from 'ws'
    import { createServer } from 'http'
    
    const server = createServer()
    const wss = new WebSocketServer({ server })
    
    const clients = new Set<WebSocket>()
    
    wss.on('connection', (ws, req) => {
      clients.add(ws)
    
      ws.on('message', (data) => {
        // Broadcast para todos os clientes conectados
        for (const client of clients) {
          if (client !== ws && client.readyState === WebSocket.OPEN) {
            client.send(data.toString())
          }
        }
      })
    
      ws.on('close', () => {
        clients.delete(ws)
      })
    
      // Ping/pong para manter conexão viva
      const pingInterval = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) ws.ping()
      }, 30000)
    
      ws.on('close', () => clearInterval(pingInterval))
    })
    
    server.listen(3001)

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes