WebSocket-связь давно перестала быть экзотикой: чаты, нотификации, живые дашборды и совместное редактирование документов давно стали стандартом. До недавнего времени Laravel-разработчикам приходилось выбирать между платным Pusher, самодельной обвязкой вокруг Soketi или экспериментами с Ratchet и Swoole. С релизом Laravel Reverb ситуация изменилась: команда Laravel выпустила официальный WebSocket-сервер, который полностью совместим с протоколом Pusher, отлично интегрируется с Laravel Echo и при этом работает на привычном PHP-стеке.
В этой статье разберём весь путь: установку, настройку broadcasting, фронтенд на Echo, приватные и presence-каналы, запуск Reverb как сервиса, масштабирование через Redis Pub/Sub и пару практических трюков для прода.
Что такое Laravel Reverb
Reverb — это самостоятельный WebSocket-сервер, написанный на PHP с использованием ReactPHP. Он реализует протокол Pusher Channels, поэтому в качестве клиента можно использовать стандартный laravel-echo с драйвером pusher-js, не подключая никаких сторонних SDK. Reverb разрабатывается командой Laravel и официально поддерживается в составе фреймворка, что снимает большую часть рисков при выборе технологии для long-running соединений.
Ключевые отличия от альтернатив:
- работает на PHP без отдельной Node.js-инфраструктуры;
- горизонтально масштабируется через Redis Pub/Sub;
- поддерживает public-, private- и presence-каналы из коробки;
- интегрируется с системой broadcasting Laravel без костылей;
- есть встроенные метрики и Pulse-карточка для мониторинга.
Требования и установка
Для запуска понадобится PHP 8.2+, Laravel 11+ и расширения ext-sockets и ext-pcntl. Если вы используете официальные docker-образы php:8.3-cli или php:8.3-fpm, оба расширения нужно собрать вручную — об этом ниже.
Устанавливаем Reverb через artisan-команду install:
php artisan install:broadcasting
Artisan задаст несколько вопросов: какой драйвер использовать (выбираем reverb), нужно ли установить Node-зависимости (соглашаемся, чтобы получить актуальные версии laravel-echo и pusher-js), а также добавить ли пример Vue/React-компонента. После выполнения команды:
- в
config/broadcasting.phpпоявится секцияreverb; - в
.envдобавятся переменныеREVERB_APP_ID,REVERB_APP_KEY,REVERB_APP_SECRET,REVERB_HOST,REVERB_PORT,REVERB_SCHEME; - будет включён
BroadcastServiceProviderи подключён файлroutes/channels.php; - в
resources/js/bootstrap.jsпоявится готовая инициализация Echo.
Запускаем сервер:
php artisan reverb:start --host=0.0.0.0 --port=8080
В консоли появится строка вида INFO Starting server on 0.0.0.0:8080 (development). Теперь Reverb принимает WebSocket-соединения на порту 8080.
Конфигурация broadcasting
В .env переключаем драйвер вещания на Reverb:
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=local-app
REVERB_APP_KEY=base64:randomly-generated-key
REVERB_APP_SECRET=base64:another-random-secret
REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=https
Обратите внимание: для прода REVERB_HOST должен указывать на ваш публичный домен, а соединение завернуто в TLS. На локальной машине удобнее работать с localhost, 8080 и схемой http.
Раздел config/reverb.php позволяет описать несколько приложений (мультитенантный сценарий), задать лимиты пинга, активировать scaling через Redis и разрешить определённые origin-домены:
'apps' => [
'apps' => [
[
'app_id' => env('REVERB_APP_ID'),
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['app.example.com'],
'ping_interval' => 60,
'max_message_size' => 10_000,
],
],
],
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', 0),
],
],
Первое событие и broadcast
Создадим событие, которое будет уходить во все подключённые браузеры. Например, новое сообщение в чате:
php artisan make:event MessageSent
Заполняем класс:
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Message $message) {}
public function broadcastOn(): array
{
return [new Channel('chat.' . $this->message->room_id)];
}
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'author' => $this->message->user->name,
'sent_at' => $this->message->created_at->toIso8601String(),
];
}
}
Дальше — отправляем событие из контроллера:
public function store(StoreMessageRequest $request, Room $room)
{
$message = $room->messages()->create([
'user_id' => $request->user()->id,
'body' => $request->validated('body'),
]);
broadcast(new MessageSent($message))->toOthers();
return response()->noContent();
}
Метод toOthers() исключает из доставки сокет-отправителя — это удобно, когда отправитель уже добавил сообщение в локальное состояние через optimistic UI.
Frontend на Laravel Echo
Если вы согласились с инсталлятором, в resources/js/bootstrap.js уже есть базовый код. Если нет — добавим вручную:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
wssPort: import.meta.env.VITE_REVERB_PORT,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
Не забываем продублировать переменные с префиксом VITE_ в .env, иначе Vite не отдаст их в бандл:
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
Подписываемся на канал из любого компонента:
window.Echo.channel(`chat.${roomId}`)
.listen('MessageSent', (event) => {
store.commit('chat/append', event);
});
Приватные и presence-каналы
Public-каналы хороши для публичных лент, но в большинстве приложений нужна авторизация. Laravel предоставляет два специальных типа каналов:
- Private — доступ только авторизованным пользователям с подтверждённым правом подписаться;
- Presence — то же самое плюс список присутствующих участников.
Описываем правила в routes/channels.php:
use App\Models\Room;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('chat.{roomId}', function ($user, int $roomId) {
return Room::findOrFail($roomId)->hasMember($user);
});
Broadcast::channel('presence-room.{roomId}', function ($user, int $roomId) {
if (! Room::findOrFail($roomId)->hasMember($user)) {
return false;
}
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
];
});
В событии меняем тип канала:
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
public function broadcastOn(): array
{
return [
new PrivateChannel('chat.' . $this->message->room_id),
new PresenceChannel('presence-room.' . $this->message->room_id),
];
}
На фронте используем private() и join():
window.Echo.private(`chat.${roomId}`)
.listen('MessageSent', handler);
window.Echo.join(`presence-room.${roomId}`)
.here((users) => store.commit('presence/init', users))
.joining((user) => store.commit('presence/add', user))
.leaving((user) => store.commit('presence/remove', user));
Авторизация подписки выполняется HTTP-запросом на /broadcasting/auth, который Laravel поднимает автоматически. Если приложение работает на нескольких поддоменах, не забудьте корректно настроить Sanctum или сессионные cookie.
Запуск Reverb как сервиса
В разработке достаточно выполнить php artisan reverb:start, но на проде сервер должен подниматься автоматически и переживать падения. Самый простой вариант — systemd-юнит:
# /etc/systemd/system/reverb.service
[Unit]
Description=Laravel Reverb WebSocket server
After=network.target redis.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php /var/www/app/artisan reverb:start --host=0.0.0.0 --port=8080
Restart=always
RestartSec=3
KillSignal=SIGINT
TimeoutStopSec=15
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Перезагружаем systemd и включаем юнит:
sudo systemctl daemon-reload
sudo systemctl enable --now reverb.service
sudo systemctl status reverb.service
Альтернатива — Supervisor с конфигурацией autorestart=true и stopwaitsecs=10. Принцип тот же: один процесс php artisan reverb:start, ровно как и для очередей.
Reverb в Docker
Удобно держать Reverb в отдельном контейнере, который шарит код с PHP-FPM, но запускает другой процесс. Минимальный Dockerfile (расширения для long-running PHP):
FROM php:8.3-cli-alpine
RUN apk add --no-cache linux-headers $PHPIZE_DEPS \
&& docker-php-ext-install sockets pcntl \
&& apk del $PHPIZE_DEPS
WORKDIR /app
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
CMD ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"]
А вот фрагмент docker-compose.yml, где Reverb стоит рядом с приложением и Redis:
services:
app:
build: .
volumes:
- ./:/app
depends_on:
- redis
reverb:
build:
context: .
dockerfile: docker/reverb.Dockerfile
volumes:
- ./:/app
environment:
REVERB_SCALING_ENABLED: "true"
REDIS_HOST: redis
depends_on:
redis:
condition: service_healthy
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/up"]
interval: 30s
timeout: 5s
retries: 3
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
Reverb отдаёт healthcheck по адресу /up, что удобно как для Docker, так и для Kubernetes liveness/readiness-проб.
Масштабирование через Redis Pub/Sub
Один процесс Reverb отлично держит тысячи соединений, но рано или поздно потребуется горизонтальное масштабирование: несколько инстансов за балансировщиком. Чтобы события доходили до всех клиентов, независимо от того, к какому процессу они подключены, включаем scaling:
REVERB_SCALING_ENABLED=true
REVERB_SCALING_CHANNEL=reverb
REDIS_HOST=redis
REDIS_PORT=6379
В этом режиме каждый процесс Reverb становится подписчиком Redis-канала и пересылает события всем своим клиентам. Балансировщик (Nginx, Traefik, HAProxy) должен поддерживать sticky-сессии — иначе клиент рискует переподключаться к разным процессам и терять presence-состояние.
Пример upstream-секции для Nginx:
upstream reverb_backend {
ip_hash;
server reverb-1:8080;
server reverb-2:8080;
server reverb-3:8080;
}
server {
listen 443 ssl http2;
server_name ws.example.com;
ssl_certificate /etc/letsencrypt/live/ws.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ws.example.com/privkey.pem;
location / {
proxy_pass http://reverb_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
Директива ip_hash обеспечивает sticky-балансировку, а большие таймауты не дают Nginx закрыть «висящие» соединения.
Дебаг и мониторинг
Reverb выводит подробные логи при запуске с флагом --debug:
php artisan reverb:start --debug
В логе видно подключения, подписки, исходящие события и ошибки авторизации — на этапе разработки этого достаточно. На проде логи лучше отправлять в системный journal через systemd либо собирать stdout контейнера через docker logging driver.
Если у вас подключён Laravel Pulse, добавьте рекордер событий Reverb в config/pulse.php:
'recorders' => [
\Laravel\Reverb\Pulse\Recorders\ReverbConnections::class => [
'sample_rate' => 1,
],
\Laravel\Reverb\Pulse\Recorders\ReverbMessages::class => [
'sample_rate' => 1,
],
],
На дашборде Pulse появятся карточки с числом активных соединений и пропускной способностью — это закрывает большинство вопросов «а что у нас сейчас с WebSocket-ами?».
Чек-лист для прода
- включите TLS и проксируйте Reverb через Nginx/Traefik с поднятым
Upgrade; - задайте
allowed_originsи не оставляйте*в проде; - увеличьте
LimitNOFILEв systemd иulimit -nв контейнере — для тысяч соединений потребуется 65535 или больше; - включите
REVERB_SCALING_ENABLED=trueпри работе нескольких процессов; - настройте мониторинг healthcheck-эндпойнта
/upв Uptime Kuma, Pulse или Prometheus blackbox-exporter; - отделите Reverb от FPM-контейнера, чтобы релизы приложения не вызывали разрывы long-running соединений;
- храните
REVERB_APP_SECRETв секретах CI/CD, не в git.
Итоги
Laravel Reverb закрывает давнюю боль PHP-экосистемы: больше не нужно платить Pusher за каждое сообщение или поддерживать сторонние Node-серверы. Конфигурация умещается в пару переменных окружения, фронтенд работает на привычном Echo, а масштабирование сводится к включению Redis Pub/Sub и sticky-балансировки. Если ваше приложение уже использует Laravel и нуждается в real-time — миграция на Reverb займёт буквально несколько часов и избавит проект от лишней инфраструктуры.
В следующих статьях мы рассмотрим, как добавить к Reverb очереди и события Eloquent через shouldBroadcast(), а также покажем тонкую настройку Pulse для отслеживания деградаций. Если вы уже запускаете Reverb в проде — поделитесь опытом в комментариях, особенно интересны кейсы с большим числом одновременных соединений и нестандартными балансировщиками.