During a production deployment of my Laravel application, I ran into a strange situation: every VITE_REVERB_* variable was present in the server .env, but the frontend still failed with a Pusher error.
My first assumption was that checking the server .env would be enough. The values were there.
The actual problem was somewhere else: the Vite bundle was not being built on the production server. It had already been built inside the GitLab CI Runner.
This is the fourth part of the series. In the previous article, I covered an Inertia SSR case where only the 404 page consistently reproduced the error. During the same integration work, another issue appeared — this time around the build-time environment rather than SSR itself.
The Browser Error That Started It
After deployment, the application loaded and routing was already working, but the browser console showed a Pusher error.
It looked like a Reverb configuration problem. My first step was therefore to inspect the production .env and confirm that the app key, host, port and scheme were present.
They were.
You must pass your app key when you instantiate Pusher.The Server .env Looked Correct
I checked the server separately to confirm that all required Reverb and Vite variables were set without printing their actual values into the logs.
for key in \ REVERB_APP_KEY \ REVERB_HOST \ REVERB_PORT \ REVERB_SCHEME \ VITE_REVERB_APP_KEY \ VITE_REVERB_HOST \ VITE_REVERB_PORT \ VITE_REVERB_SCHEMEdo value=$(grep -m1 "^${key}=" .env | cut -d= -f2-) if [ -n "$value" ]; then echo "$key=SET" else echo "$key=EMPTY_OR_MISSING" fidoneThe Key Detail: Vite Needed the Values at Build Time
The frontend read its Reverb settings through import.meta.env.
That meant the required VITE_* values had to exist in the environment running npm run build. In my pipeline, that process was running inside the GitLab Runner.
The production server .env belonged to a completely different stage and could no longer change an already generated JavaScript bundle.
window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST ?? window.location.hostname, wsPort: import.meta.env.VITE_REVERB_PORT ?? 80, wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'http') === 'https',});A correct server .env cannot help a frontend bundle that was built earlier in a different environment.
I Checked Where the Build Actually Runs
Next, I opened .gitlab-ci.yml and followed the deployment flow in order.
That is where the real cause became obvious. npm ci and npm run build ran in the GitLab Runner, and only afterwards were the generated public/build and SSR artifacts copied to the server.
The production server was receiving the build output, not building the frontend again with its own .env.
script: - npm ci - npm run build # generated artifacts are transferred afterwards - rsync public/build/ ... - rsync bootstrap/ssr/ ...GitLab Runner
↓
npm run build
↓
VITE_* missing
↓
broken frontend bundle
↓
production server
GitLab Runner
↓
production-scoped VITE_*
↓
npm run build
↓
correct frontend bundle
↓
production server
The Fix Was in GitLab CI Variables, Not the Server .env
At that point, I stopped looking for the problem in the production .env.
I added the VITE_* variables required by the frontend build to GitLab CI/CD Variables. Development and production received separate environment scopes so each deployment could build with its own values.
The server-side REVERB_* configuration remained part of the server environment.
Variable | Де потрібна | Scope |
|---|---|---|
VITE_REVERB_APP_KEY | Vite frontend build |
|
VITE_REVERB_HOST | Vite frontend build |
|
VITE_REVERB_PORT | Vite frontend build |
|
VITE_REVERB_PORT | Vite frontend build |
|
REVERB_APP_SECRET | Laravel / Reverb server | server only |
I Separated Development and Production With Environment Scopes
I did not want one global set of VITE_REVERB_* values to be shared by every deployment.
For each key, I created one entry scoped to development and another scoped to production. That lets the same pipeline build the frontend with the correct host, port, scheme and app key for each environment.
It also reduces the risk of accidentally building a production bundle with development configuration.
VITE_REVERB_APP_KEY → development
VITE_REVERB_APP_KEY → production
VITE_REVERB_HOST → development
VITE_REVERB_HOST → production
VITE_REVERB_PORT → development
VITE_REVERB_PORT → production
VITE_REVERB_SCHEME → development
VITE_REVERB_SCHEME → production
I Added Fail-Fast Checks
I did not want this to become a problem that could silently return when someone removed a variable later.
So I added simple shell checks before the build. If even one required value is missing, the deployment stops immediately.
For me, that is much better than a green pipeline producing a JavaScript bundle that only fails later in the browser.
script: - ': "${VITE_INERTIA_SSR_PORT:?VITE_INERTIA_SSR_PORT is required}"' - ': "${VITE_REVERB_APP_KEY:?VITE_REVERB_APP_KEY is required}"' - ': "${VITE_REVERB_HOST:?VITE_REVERB_HOST is required}"' - ': "${VITE_REVERB_PORT:?VITE_REVERB_PORT is required}"' - ': "${VITE_REVERB_SCHEME:?VITE_REVERB_SCHEME is required}"' - ': "${HEALTHCHECK_URL:?HEALTHCHECK_URL is required}"' - npm ci - npm run buildThese checks do not fix the configuration. They make it impossible for an invalid configuration to deploy silently.
Why Fail-Fast Was Better Than Another Fallback
The frontend configuration already had fallback values for host, port and scheme. But a fallback is not appropriate for everything.
If VITE_REVERB_APP_KEY is missing, the browser cannot guess which key it should use. The Pusher error was therefore a useful symptom, not a place where I wanted to add another workaround.
I chose to stop the build rather than hide a missing required variable.
If the application cannot work correctly without a variable, the pipeline should say so before deployment.
I Verified It on Development First
I first added the environment-scoped variables for development and ran the normal deployment.
The pipeline passed the fail-fast checks, the frontend and SSR bundles built successfully, and after a hard reload the Pusher error disappeared from the browser.
Only then did I repeat the same setup for the production scope.
I Checked Production Variables Before the Merge
Before merging into main, I added the same four VITE_REVERB_* keys with production scope and production values.
This mattered because the development setup was already working, but production should never accidentally inherit development values.
The production deployment then completed the build, Composer update, cache rebuild and restart of the required Supervisor processes.
Worker status: RUNNINGReverb status: RUNNINGSSR status: RUNNINGSupervisor processes are running. Deployment completed successfully.Job succeededI Verified More Than the Pipeline
A green GitLab job was not enough for me to call the deployment finished.
I separately checked the production revision, the Inertia SSR process, Supervisor services, public UK/EN navigation and the admin area. The browser console remained clean throughout the smoke test.
Then I placed a marker in the Laravel log and inspected only entries that could have appeared after that smoke test.
php artisan inertia:check-ssrInertia SSR server is running.sudo supervisorctl status | grep '^icanup'After the production smoke test, the browser console had no errors and the controlled log check showed no new Laravel errors.
This Was Not My First GitLab Pipeline Change
I had already been building and verifying this deployment pipeline step by step, which I described in my article about a real Laravel CI/CD pipeline. This time the question was narrower: not how to deploy the application, but where the frontend actually receives its environment configuration.
What I Kept for Next Time
First identify where the build actually runs.
Do not confuse the server runtime .env with the Vite build-time environment.
Use separate environment scopes for development and production.
Fail fast on required build variables before npm run build.
Never expose server secrets through VITE_*.
After deployment, verify the browser, services and fresh Laravel logs - not only the green pipeline.
The Previous Part of the Series
Before this, I covered a different deployment-time edge case: why only the Laravel 404 page was breaking Inertia SSR. That problem appeared only in the error flow; this one came down to where the Vite build physically ran.
Conclusion
What confused me most was that the production .env was correct. I was looking at the right values, but at the wrong stage of the application lifecycle.
Vite needed the VITE_* variables while the build was running inside the GitLab Runner. By the time the generated bundle reached the server, changing the server .env was already too late.
Since then, I have been much more deliberate about separating runtime configuration from build-time configuration. Required frontend variables are now validated by the pipeline before deployment can ever reach the server.
The real question was not “Is the variable in .env?” but “Was it available where npm run build actually ran?”.



