Reverb is a long-running WebSocket server, not a development terminal command promoted to production. Install and publish its configuration, then make the public and internal addresses explicit in config/reverb.php and the REVERB_* environment values.
REVERB_APP_ID=app-id
REVERB_APP_KEY=app-key
REVERB_APP_SECRET=app-secret
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=https
The SERVER_* values describe the process binding inside Docker; the public values describe what Echo and clients reach through TLS. Proxy WebSocket upgrade headers, expose WSS only, and restrict allowed origins to real application origins. Keep credentials server-side except the public key needed by the client build.
Make public and internal addresses separate
Publish config/reverb.php and keep the bind address distinct from the address
Echo reaches. The process listens on Docker's private network; browsers use the
TLS endpoint through Nginx. Build-time VITE_ variables are public metadata,
never application secrets.
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=https
VITE_REVERB_APP_KEY=${REVERB_APP_KEY}
VITE_REVERB_HOST=ws.example.com
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https
location /app/ {
proxy_pass http://reverb:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 60s;
}
Prove the whole broadcast path
An event, its channel authorization and the browser listener form one feature. Keep queue workers running: queued broadcasts do not appear merely because the WebSocket process is healthy.
<?php
declare(strict_types=1);
namespace App\\Events;
use App\\Models\\Order;
use Illuminate\\Broadcasting\\PrivateChannel;
use Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;
final class OrderStatusChanged implements ShouldBroadcast
{
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [new PrivateChannel("orders.{$this->order->getKey()}")];
}
}
window.Echo.private(`orders.${orderId}`).listen('OrderStatusChanged', (event: { order: { status: string } }) => {
setStatus(event.order.status);
});
Scaling is a shared-state decision
One instance is a valid first production deployment. Multiple Reverb instances require the documented horizontal-scaling path and a shared backend so every node can see broadcasts and channel state; a load balancer alone cannot make isolated processes coherent. Use a Redis broadcasting path where appropriate, deploy configuration consistently, and test two connections routed to different instances before claiming scale-out works.
Supervise php artisan reverb:start with a container restart policy or process manager, give it a health signal, collect structured logs and drain/restart deliberately during deployments. Monitor connections, reconnects, event latency, CPU, file descriptors and 4xx/5xx proxy responses.
Production failures to rehearse
Check WSS from a browser, private-channel authorization, queue latency and a reconnect during a rolling restart. Monitor connection count, memory, file descriptors, Nginx upgrade failures and Redis connectivity. One node is a sound starting point; before adding replicas, use the documented shared Redis scaling path and prove two clients routed to different instances receive the event.
When not to use Reverb
Do not use WebSockets for a page that can refresh on demand or poll cheaply. Do not expose the internal port directly to the internet. For a globally distributed, managed-reliability requirement, a hosted compatible broadcaster can be a better operational trade-off than owning Reverb capacity and incident response.