0% прочитано

Vite Environment Variables in Laravel Deployments: Why Server .env Was Not Enough

All VITE_REVERB_* values were present in the server .env, yet the production bundle was still built without them. I traced the problem to the GitLab CI build stage, moved the build-time variables into the correct environment scope, and added fail-fast checks before deployment.

28 серпня 2026 р. 7 хв читанняCI/CD

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.

The server .env was correct. But by the time deployment reached the production server, the frontend bundle had already been built without the required VITE_* values.

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.

Browser error - missing Pusher app key
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.

BASH - Check Reverb variables on the server
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"    fidone
For a public article, I do not expose real environment values. For debugging, it is enough to confirm that a variable exists; secrets do not belong in screenshots, logs or examples.

The 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.

JavaScript - Reverb configuration in the frontend
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.

YAML - simplified deployment flow
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

development / production

VITE_REVERB_HOST

Vite frontend build

development / production

VITE_REVERB_PORT

Vite frontend build

development / production

VITE_REVERB_PORT

Vite frontend build

development / production

REVERB_APP_SECRET

Laravel / Reverb server

server only

Do not move server secrets into VITE_* variables. Anything exposed through VITE_* can end up in the client bundle. REVERB_APP_SECRET must remain server-side 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 now treat environment scope for build-time variables as part of the deployment configuration, not just a convenient way to organize GitLab Variables.

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.

YAML - fail-fast environment checks
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 build

These 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.

After the new development build, “You must pass your app key when you instantiate Pusher” no longer reproduced.

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.

Production deployment - final status
Worker status: RUNNINGReverb status: RUNNINGSSR status: RUNNINGSupervisor processes are running. Deployment completed successfully.Job succeeded

I 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.

BASH - Check Inertia SSR
php artisan inertia:check-ssr
Output - Inertia SSR
Inertia SSR server is running.
BASH - Check Supervisor processes
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.

The production bundle now had the correct build-time configuration: Reverb, SSR and navigation all passed the smoke test without new browser or Laravel errors.

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?”.