Nginx и WebSocket proxy: почему локально работает, а на сервере нет

Локально WebSocket работает:.

Ниже — разделы про типичная ситуация, почему WebSocket требует особой настройки и минимальный конфиг, чтобы быстро понять устройство материала, практические ограничения и типовые точки отказа.


Типичная ситуация

Локально WebSocket работает:

ws://localhost:3000

Вы выкатываете проект на сервер, открываете домен, а браузер пишет ошибку. REST API отвечает, страницы грузятся, но WebSocket не подключается

Частая причина — Nginx проксирует обычные HTTP-запросы, но не передает WebSocket Upgrade как надо

Почему WebSocket требует особой настройки

WebSocket начинается как HTTP-запрос, но затем соединение "апгрейдится" до WebSocket. Для этого нужны заголовки:

  • Upgrade;
  • Connection;
  • HTTP/1.1 для proxy.

Если Nginx не передает эти заголовки backend-серверу, WebSocket handshake не проходит

Минимальный конфиг

Допустим, Node.js/FastAPI сервер слушает 127.0.0.1:3000, а WebSocket endpoint находится на /ws

Пример:

server {
    listen 80;
    server_name example.com;

    location /ws {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Ключевые строки:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Без них обычные HTTP-запросы могут работать, а WebSocket — нет

Более аккуратный вариант с map

В официальной документации Nginx часто используют map, чтобы корректно задавать Connection

В блоке http:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
location /ws {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
}

Такой вариант лучше, если в конфиге есть разные сценарии соединений

ws:// и wss://

Если сайт открыт по HTTPS, клиент должен подключаться через:

wss://example.com/ws

А не:

ws://example.com/ws

Иначе браузер может заблокировать mixed content

Nginx в этом случае принимает TLS-соединение снаружи, а внутрь может проксировать на локальный backend:

location /ws {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
}

Снаружи для браузера это wss://example.com/ws, внутри для Nginx — обычный http://127.0.0.1:3000

Таймауты

WebSocket-соединение может быть долгим. Если Nginx закрывает его слишком быстро, добавьте таймауты:

location /ws {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Но таймауты не должны заменять нормальный heartbeat. В реальном приложении полезно периодически проверять живость соединения

Node.js пример за Nginx

Backend слушает локальный порт:

const { WebSocketServer } = require('ws');

const port = Number(process.env.PORT || 3000);
const wss = new WebSocketServer({ port });

wss.on('connection', (socket) => {
  socket.send('connected');
});

console.log(`WebSocket server on ${port}`);

Снаружи клиент подключается не к :3000, а к домену:

const socket = new WebSocket('wss://example.com/ws');

Если Nginx location /ws проксирует на backend, путь должен совпадать с тем, как backend принимает соединение. Если backend слушает корень, иногда нужно аккуратно настроить path или proxy_pass

FastAPI пример за Nginx

FastAPI:

from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    await websocket.send_text("connected")

Uvicorn:

uvicorn main:app --host 127.0.0.1 --port 8000

Nginx:

location /ws {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host $host;
}

Клиент:

const socket = new WebSocket('wss://example.com/ws');

Диагностика

Проверить backend напрямую

Если есть доступ к серверу, проверьте локально:

curl http://127.0.0.1:3000

Для WebSocket лучше использовать специальный клиент, Postman или простой Node/Python-клиент

Проверить логи Nginx

Смотрите access/error logs. Если запрос до backend не дошел, это видно

Проверить браузер

В DevTools откройте Network и найдите WebSocket-запрос. Важно увидеть:

  • какой URL реально используется;
  • какой статус handshake;
  • есть ли frames;
  • какой close code.

Проверить протокол

На HTTPS-сайте должен быть wss://

Частые ошибки

Забыли proxy_http_version 1.1

Без HTTP/1.1 Upgrade может не пройти

Не передали Upgrade

Строка обязательна:

proxy_set_header Upgrade $http_upgrade;

Неправильный Connection

Для простого конфига:

proxy_set_header Connection "upgrade";

Для более аккуратного:

proxy_set_header Connection $connection_upgrade;

Клиент подключается к неправильному path

На backend /ws, в клиенте /socket, в Nginx /websocket — и все молча расходится. Приведите path к одной схеме

Сервер слушает внешний порт вместо localhost

Это не всегда ошибка, но на VPS часто backend держат на 127.0.0.1, а наружу открывают только Nginx

Мини-чеклист

Перед тем как копаться в коде приложения, проверьте:

  1. Клиент использует wss:// на HTTPS-домене.
  2. Nginx передает Upgrade.
  3. Nginx передает Connection.
  4. Стоит proxy_http_version 1.1.
  5. Path совпадает.
  6. Backend запущен.
  7. Порт backend совпадает с proxy_pass.
  8. В логах backend видно подключение.
  9. В логах Nginx нет 400/502/504.
  10. Хостинг вообще поддерживает долгие соединения.

Ответы на эти вопросы могут быть для вас полезными

Почему REST API работает, а WebSocket нет?

Потому что WebSocket требует Upgrade handshake. Обычный HTTP proxy может пропускать REST и ломать WebSocket

Нужно ли открывать порт Node.js наружу?

Обычно нет. Часто наружу открыт Nginx на 80/443, а Node.js слушает localhost-порт

Что такое proxy_read_timeout?

Это время ожидания чтения от proxied-сервера. Для долгих WebSocket-соединений его часто увеличивают

Почему нужен wss://?

На HTTPS-странице браузер не должен подключаться к небезопасному ws://. wss:// — защищенный WebSocket

Можно ли проксировать несколько WebSocket endpoint?

Да. Делайте разные location, например /ws/chat и /ws/notifications, или маршрутизируйте внутри приложения

Что почитать дальше по WebSockets

Если вы собираете тему по шагам, рядом лучше открыть:

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x