Когда MQTT выходит за пределы устройств и попадает в браузер, нам неизбежно нужен WebSocket. Браузеры не умеют в сырой TCP на порту 1883, зато отлично работают с ws:// и wss://. Самый простой и управляемый путь — поставить брокер Mosquitto за Nginx, а на периметре завершать TLS и делать nginx reverse proxy для WebSocket апгрейда. В этой инструкции соберём рабочую схему, разберём конфиг, таймауты, keepalive, тестирование и типичные грабли.
Зачем MQTT over WebSocket и почему через Nginx
MQTT поверх WebSocket обеспечивает доступ из веб-клиентов без лишних костылей. Дальше — вопрос удобства эксплуатации и безопасности. Завершая TLS на Nginx, мы получаем:
- Единый TLS-терминатор и понятный контроль сертификатов, шифров и HSTS.
- Гибкую маршрутизацию и защиту от всплесков на уровне HTTP (лимиты, логирование, доступ по Origin).
- Удобные таймауты и читаемые логи, пригодные для централизованного сбора.
- Простой путь к масштабированию за счёт upstream-групп или перенаправлений.
Брокер Mosquitto остаётся внутри, слушает websockets-листенер только на 127.0.0.1 (или в приватной сети), а наружу выходит только Nginx с wss://. Это и безопасно, и прозрачно. Разворачивать связку удобно на облачном VDS, особенно если нужна изоляция и контроль ресурсов.
Архитектура: потоки и порты
Базовая раскладка портов в типичной инсталляции:
- Клиент → Nginx:
443/tcp(wss://, TLS завершаем в Nginx). - Nginx → Mosquitto:
9001/tcp(ws://до локальногоwebsockets-листенера). - Локальные или приватные издатели/подписчики: по желанию
1883/tcpбез TLS только внутри контура.
Не открывайте наружу порт Mosquitto
9001(websockets) и, как правило,1883(raw MQTT). Для внешнего трафика достаточноwss://на 443/TCP через Nginx.

Настраиваем Mosquitto для WebSocket
Пример минимального /etc/mosquitto/mosquitto.conf для работы за Nginx. Включаем аутентификацию, локальный TCP и websockets-листенер, ограничим keepalive и размер сообщений.
# /etc/mosquitto/mosquitto.conf
per_listener_settings true
# Без анонимов
allow_anonymous false
password_file /etc/mosquitto/passwd
# Персистентность (по желанию)
persistence true
persistence_location /var/lib/mosquitto/
# Локальный raw MQTT (для внутренних агентов)
listener 1883 127.0.0.1
# WebSocket для Nginx
listener 9001 127.0.0.1
protocol websockets
# Полезные лимиты
max_keepalive 600
message_size_limit 1048576
# Логи
log_type error
log_type warning
log_type notice
log_timestamp true
Создаём парольный файл и пользователя:
sudo mosquitto_passwd -c /etc/mosquitto/passwd iot_admin
sudo systemctl restart mosquitto
Для узлов с большим числом одновременных соединений увеличьте лимиты файловых дескрипторов через drop-in для systemd:
sudo systemctl edit mosquitto
[Service]
LimitNOFILE=65536
Restart=always
RestartSec=2s
sudo systemctl daemon-reload
sudo systemctl restart mosquitto
Помните, что Mosquitto не перечитывает весь конфиг по SIGHUP; существенные изменения требуют рестарта службы.
Nginx: TLS и reverse proxy для WebSocket
Нам нужен корректный апгрейд до WebSocket (заголовки Upgrade и Connection), адекватные таймауты и выключенное буферизование. TLS завершаем на периметре, выставляем современные протоколы и защищённые настройки сессий. Для публичного доступа используйте доверенный сертификат из раздела SSL-сертификаты. Если параллельно настраиваете HSTS и перенос домена — пригодится разбор переезда домена с 301 и HSTS.
# В http{}.
map $http_upgrade $connection_upgrade {
default upgrade
'' close
}
# Локальный upstream до Mosquitto WebSocket listener
upstream mosquitto_ws {
server 127.0.0.1:9001 max_fails=2 fail_timeout=10s;
keepalive 32;
}
# Необязательный фильтр по Origin (замените example.com на ваш домен)
map $http_origin $ws_allowed {
default 0;
~^https?://(www\.)?example\.com$ 1;
}
# Лимит логирования только рукопожатий 101 (по желанию)
map $status $ws_handshake { ~^101$ 1; default 0; }
log_format mqtt '$remote_addr - $host [$time_local] "$request" $status $body_bytes_sent '
'"$http_user_agent" "u=$http_upgrade" "c=$connection"';
server {
listen 443 ssl http2;
server_name mqtt.example.com;
# TLS (заглушки путей под ваши сертификаты)
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
access_log /var/log/nginx/mqtt_access.log mqtt if=$ws_handshake;
error_log /var/log/nginx/mqtt_error.log warn;
# Точка подключения WebSocket клиентов
location /mqtt {
# Необязательная проверка Origin
if ($ws_allowed = 0) { return 403; }
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 5s;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
# WS не буферизуем
proxy_buffering off;
# Не переключаем upstream при сбоях — WS плохо переживает
proxy_next_upstream off;
proxy_pass http://mosquitto_ws;
}
}
Несколько замечаний:
proxy_read_timeoutдолжен быть больше, чем MQTT keepalive клиента. Если клиенты посылают PINGREQ каждые 60 секунд, ставьте хотя бы 5–10 минут или больше под ваш сценарий.- Для WebSocket буферизация должна быть выключена:
proxy_buffering off, иначе можно получить задержки и распухание памяти. - Журналируйте только рукопожатия
101— объём логов меньше, а полезные параметры запроса остаются. - Наличие
http2на сервере не мешает WebSocket: браузеры инициируют WS по HTTP/1.1, а обычные запросы спокойно идут по HTTP/2.
Ограничения соединений и защита от всплесков
Во время массового переподключения клиентов (например, при кратковременном сетевом сбое) полезно ограничить скорость рукопожатий и коннектов на уровне Nginx. Пример базовых лимитов:
# В http{}
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_req_zone $binary_remote_addr zone=wsreq:10m rate=5r/s;
server {
# ...
location /mqtt {
limit_conn perip 20;
limit_req zone=wsreq burst=10 nodelay;
# ... остальной конфиг из примера выше
}
}
Значения подбирайте под поведение клиентов и ожидаемую нагрузку. Важен баланс: не «убить» легитимный шквал переподключений, но и не допустить лавинообразной нагрузки. Если нужен удобный GUI для сервера — пригодится сравнение панелей для VDS.
Keepalive и timeouts: практические пресеты
MQTT keepalive — это интервал, с которым клиент посылает PINGREQ, чтобы брокер (и все прокси) знали, что соединение живо. Если прокси не видит трафика дольше своего read_timeout, он закроет соединение.
- Для браузеров и мобильных клиентов ставьте keepalive 30–60 секунд. В JS-клиентах это обычно
keepAliveInterval. - В Nginx держите
proxy_read_timeoutминимум в 5–10 раз больше клиентского keepalive (например, 10–30 минут). Для нестабильных сетей иногда требуется 1–2 часа. - У Mosquitto лимитируйте
max_keepalive(например, 600 секунд), чтобы плохо настроенные клиенты не висели без пингов. - TCP keepalive ядра (
net.ipv4.tcp_keepalive_*) полезен для «застрявших» соединений на уровне NAT/фаерволов, но не заменяет MQTT keepalive.
Пример системных настроек TCP keepalive (опционально, для серверов с большим количеством долгоживущих соединений):
# /etc/sysctl.d/99-mqtt-ws.conf
net.ipv4.tcp_keepalive_time=600
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=5
Применить:
sudo sysctl --system

Тестируем: CLI и браузер
Проверим, что проксирование wss:// работает. Начнём с CLI-утилит из пакета Mosquitto. Система должна доверять вашему сертификату (или укажите путь к корневым сертификатам).
# Подписка через WSS на 443
mosquitto_sub -h mqtt.example.com -p 443 --capath /etc/ssl/certs --protocol websockets -t 'test/hello' -v -u iot_admin -P 'secret'
# Публикация сообщения
mosquitto_pub -h mqtt.example.com -p 443 --capath /etc/ssl/certs --protocol websockets -t 'test/hello' -m 'hi from wss' -u iot_admin -P 'secret'
В браузере (например, Paho MQTT JS) это выглядит так:
// Пример инициализации Paho MQTT JS по WSS через Nginx
const clientId = "web_" + Math.random().toString(16).slice(2, 10);
const client = new Paho.MQTT.Client("mqtt.example.com", 443, "/mqtt", clientId);
client.onConnectionLost = (res) => console.warn("lost", res.errorMessage);
client.onMessageArrived = (msg) => console.log(msg.destinationName, msg.payloadString);
client.connect({
useSSL: true,
userName: "iot_admin",
password: "secret",
keepAliveInterval: 60,
reconnect: true,
cleanSession: true,
onSuccess: () => {
console.log("connected");
client.subscribe("test/hello", { qos: 1 });
},
onFailure: (e) => console.error("connect failed", e)
});
Обратите внимание на путь /mqtt — он должен совпадать с локацией в конфиге Nginx. Mosquitto не использует путь при обработке WebSocket (для него это просто транспорт), но прокси — использует.
Отладка: что ломается чаще всего
- 400/403 на рукопожатии. Проверьте заголовки
UpgradeиConnection. В Nginx для WS обязательно нужныproxy_set_header Upgrade $http_upgradeиproxy_set_header Connection $connection_upgrade. Если включили фильтр по Origin — убедитесь, что он пропускает ваш сайт. - Обрыв через ровно N минут. Признак слишком малого
proxy_read_timeoutна Nginx или слишком большого MQTT keepalive у клиента. Уменьшите keepalive клиента и/или увеличьте read_timeout прокси. - "upstream prematurely closed connection" в error_log. Чаще всего брокер завершил соединение (лимиты, авторизация) или вы подключились не по WebSocket, а сырой MQTT на
/mqtt. Проверьте протокол клиента и логи Mosquitto. - Проблемы с сертификатом. Для CLI укажите
--capath /etc/ssl/certsили путь кCAfile, которым подписан серверный сертификат. В браузере используйте публично доверенный сертификат. - Зависания после апдейта.
nginx -s reloadбезопасно перезагружает конфиг без разрыва активных WS, а Mosquitto для изменений конфига чаще требуетrestart— планируйте окна.
Производительность и масштабирование
Для десятков тысяч соединений на узел следите за тремя классами ограничений: файловые дескрипторы, сетевые очереди ядра и память на прокси/брокере.
- nofile для Nginx и Mosquitto (см.
LimitNOFILEвыше). В Nginx проверьтеworker_connectionsи итоговый пределworker_processes * worker_connections. - Очереди сокетов:
net.core.somaxconn,net.ipv4.tcp_max_syn_backlog, если наблюдаете Listen queue overflow. - Логи: не логируйте каждый WS-трафик, только рукопожатия. Иначе диски и CPU уйдут в логирование.
Горизонтальное масштабирование по нескольким брокерам возможно, но требует «прилипчивости» соединений. В классическом Nginx HTTP upstream есть ip_hash, но за NAT он может работать плохо. Для MQTT часто используют маршрутизацию по ClientID на уровне приложения или брокер-кластеры с синхронизацией состояния. Если всё же балансируете через Nginx, оставляйте proxy_next_upstream off, иначе WS не переживёт переключение корректно.
Чек-лист перед продом
- Mosquitto:
allow_anonymous false, заведены пользователи, ограниченmax_keepalive, проверенаmessage_size_limit, локальныйwebsockets-листенер на127.0.0.1:9001. - Nginx: корректные заголовки Upgrade/Connection, длинные
proxy_read_timeout,proxy_buffering off, отключёнproxy_next_upstreamдля WS. - TLS: валидный сертификат, включены
TLSv1.2/1.3, HSTS, отключены session tickets, если нет ротации ключей. - Безопасность: опциональный фильтр по
Origin, лимитыlimit_req/limit_conn, закрыты наружу порты Mosquitto. - Тесты:
mosquitto_subиmosquitto_pubпоwss://, браузерный клиент, проверка реконнекта при рестарте брокера. - Мониторинг: метрики по числу соединений и задержкам, алерты на рост 4xx/5xx в Nginx и ошибки аутентификации в Mosquitto.
Итоги
Схема «Nginx как обратный прокси + Mosquitto за ним» даёт предсказуемый и безопасный способ отдать MQTT в браузер через wss://. Небольшой, но важный набор настроек — апгрейд заголовков, длинные таймауты, отключённая буферизация, аккуратные лимиты и базовая защита на периметре — избавит вас от рваных сессий и лишней возни. Дальше остаётся работать с логикой приложения и обдуманно масштабировать брокер там, где это действительно нужно.


