skip to content

When deploying Laravel Reverb behind Nginx on port 443, what is the difference between REVERB_SERVER_HOST/PORT and REVERB_HOST/PORT, and what must the proxy forward?

level: middleimportance: should knowfreq 30%

answer

  1. one pair binds, one pair addresses
  2. 0.0.0.0:8080 behind, host:443 in front
  3. VITE_ copies are built into the bundle
  4. proxy both /app and /apps
  5. Upgrade and Connection headers

basics

~20 s

REVERB_SERVER_HOST/PORT are where the Reverb process binds, such as 0.0.0.0:8080. REVERB_HOST/PORT/SCHEME are the public address the app sends broadcasts to and Echo connects to, such as ws.example.com:443 over https. The proxy must upgrade /app WebSockets and forward /apps API calls.

solid answer

~40 s

The two pairs answer different questions. `REVERB_SERVER_HOST` and `REVERB_SERVER_PORT` (defaults `0.0.0.0` and `8080` in `config/reverb.php`) tell `reverb:start` which interface and port to bind. `REVERB_HOST`, `REVERB_PORT` and `REVERB_SCHEME` tell the Laravel app where to send broadcasts (`config/broadcasting.php`, port default 443, scheme `https`), and through their `VITE_REVERB_*` copies tell Echo where browsers connect. Behind Nginx you bind Reverb to `0.0.0.0:8080` and set `REVERB_HOST=ws.example.com`, `REVERB_PORT=443`, `REVERB_SCHEME=https`, terminating TLS at the proxy. The proxy must pass **`/app`** (WebSocket connections, with `proxy_http_version 1.1` and the `Upgrade`/`Connection` headers) and **`/apps`** (the app's HTTP API calls). Because Vite inlines the `VITE_` values at build time, changing the host means rebuilding the frontend.

code

nginx · 13 lines
nginx
server {
    listen 443 ssl;
    server_name ws.example.com;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:8080;
    }
}

go deeper

for a junior

Recall that one pair of variables sets where Reverb listens and the other where clients reach it.

for a middle

Explain which config file reads each pair, why the VITE_ copies need a rebuild, and why /app and /apps both need proxying.

for a senior

Lay out a production topology with TLS at the proxy, a private bind address, and an internal path for the app's API calls when useful.

for a principal

Decide where the WebSocket tier lives relative to the web tier, which host names it gets, and how its configuration is kept consistent across builds and servers.

## Two pairs of variables, two questions Reverb's configuration keeps two addresses apart, and mixing them up is the most common deployment bug: | Variables | Question they answer | Read by | Defaults | |---|---|---|---| | `REVERB_SERVER_HOST`, `REVERB_SERVER_PORT` | where does the Reverb process **listen**? | `reverb:start` via `config/reverb.php` | `0.0.0.0`, `8080` | | `REVERB_HOST`, `REVERB_PORT`, `REVERB_SCHEME` | where do **clients reach** Reverb? | the app's broadcaster via `config/broadcasting.php` | host unset, `443`, `https` | | `VITE_REVERB_HOST`, `VITE_REVERB_PORT`, `VITE_REVERB_SCHEME` | where does the **browser** connect? | Echo, compiled into the JS bundle | copies of the `REVERB_*` values | `reverb:install` writes a local-friendly `.env` (`REVERB_HOST="localhost"`, `REVERB_PORT=8080`, `REVERB_SCHEME=http`), which is why a first deploy that forgets to change them sends browsers to `localhost`. ## A production layout for the whiteboard For a collaborative whiteboard served at `whiteboard.example.com`, a typical single-server layout is: 1. Reverb runs under a process manager and binds `0.0.0.0:8080`, a port not exposed to the internet. 2. Nginx terminates TLS for `ws.example.com` on 443 and proxies to `127.0.0.1:8080`. 3. The app and the browser both address the public name. ```ini REVERB_SERVER_HOST=0.0.0.0 REVERB_SERVER_PORT=8080 REVERB_HOST=ws.example.com REVERB_PORT=443 REVERB_SCHEME=https ``` ## What the proxy must forward Reverb serves two kinds of traffic, and both have to reach it: - **`/app/{key}`** — the WebSocket connections from browsers. The proxy must speak HTTP/1.1 to Reverb and pass the `Upgrade` and `Connection` headers, or the handshake fails. - **`/apps/{appId}/...`** — the Pusher-style HTTP API. The Laravel app posts every broadcast here, and signed calls to list channels and connections use it too. A proxy rule written only for `/app` lets browsers connect but silently drops every broadcast, because the app's `POST /apps/{id}/events` never arrives. Forwarding `/` to Reverb on a dedicated host name avoids the problem. ## The build-time trap The `VITE_REVERB_*` variables are read by Vite when `npm run build` runs and are inlined into the JavaScript bundle. Consequences: - changing `REVERB_HOST` in the production `.env` does nothing for browsers until the assets are rebuilt - a bundle built on a developer machine carries that machine's values - the build environment must have the production values, even though the server's `.env` is what the PHP side reads ## When the app talks to Reverb directly The app does not have to go through the public proxy. If the web servers can reach the Reverb host on the private network, `REVERB_HOST`/`REVERB_PORT`/`REVERB_SCHEME` can point at the internal address (for example `http` on 8080), while the `VITE_REVERB_*` variables are set explicitly to the public name. That saves a TLS round trip per broadcast, at the cost of keeping two sets of values in sync. ## Local development Locally the defaults written by `reverb:install` usually just work: Reverb binds `0.0.0.0:8080`, and `REVERB_HOST="localhost"`, `REVERB_PORT=8080` and `REVERB_SCHEME=http` point both the app and Echo straight at it with no proxy. Two variations come up: - with a local HTTPS site from Herd or Valet, set `REVERB_HOST` to the site's host name, or start the server with `--hostname`, so Reverb serves `wss://` using that site's certificate - in containers, the app container must reach Reverb by its service name, while the browser still needs a host name and port published to the host machine, which is the same split between app-facing and browser-facing addresses as in production ## Checklist - the Reverb process is listening where `REVERB_SERVER_*` says, and only the proxy can reach it - the app can reach `REVERB_HOST:REVERB_PORT` and `/apps` is forwarded - the browser bundle was built with the public `VITE_REVERB_*` values - `wss://` is terminated at the proxy, so Reverb itself needs no certificate

  • Browsers connect to Reverb in production, but no broadcast ever arrives; the proxy only has a location for /app. Why?
    The app delivers broadcasts by posting to Reverb's HTTP API under `/apps/{appId}/events`. With only `/app` proxied, those requests never reach Reverb, so connected browsers wait for messages that are never sent. Forward `/apps` too, or proxy the whole dedicated host.
  • You changed REVERB_HOST on the server and restarted everything, yet browsers still connect to the old host; why?
    Echo reads `VITE_REVERB_HOST`, which Vite inlined into the JavaScript bundle at build time. Restarting PHP processes does not change built assets. Rebuild the frontend with the new values and deploy the new bundle.

saying these in an interview costs you the question

  • REVERB_HOST is the address the Reverb process binds to.
  • Only the /app path needs to reach Reverb through the proxy.
  • Changing VITE_REVERB_HOST in .env takes effect without rebuilding assets.
  • Reverb must hold its own TLS certificate when it sits behind Nginx.
  • REVERB_SERVER_PORT should be 443 so browsers can reach Reverb directly.