a month ago
Description of the issue:
A build-time env var read via process.env inside vite.config.ts is not being applied to the build output, even though the same var is confirmed set as a service variable and confirmed visible to the build shell itself.
Setup: Node/Vite app, Nixpacks builder, buildCommand = "npm run build" (runs vite build). Service variable VITE_GA_MEASUREMENT_ID= is set (confirmed via railway variables --kv).
vite.config.ts reads it explicitly at config-evaluation time:
define: {
'import.meta.env.VITE_GA_MEASUREMENT_ID': JSON.stringify(process.env.VITE_GA_MEASUREMENT_ID ?? ''),
},
Expected: the built client bundle contains the literal string and our gtag.js loader (gated behind this value being non-empty).
Actual: across multiple real builds — including ones with a ~4,000-line application diff between them, so this isn't a stale-cache symptom — the resulting bundle never contains the value.
What I've ruled out, with evidence:
Var not reaching the build — ruled out. A temporary "echo GA_CHECK=$VITE_GA_MEASUREMENT_ID && vite build" build script printed GA_CHECK= correctly, in the same RUN step, immediately before vite build runs.
Stale Docker layer/build cache — ruled out. The RUN --mount=type=cache,...,target=/app/node_modules/.cache npm run build step genuinely re-executes each time (varying build times, and after a large application diff, a genuinely different output hash/size) — not a cached replay.
Local repro — the exact same vite.config.ts, same lockfile-resolved Vite version (6.4.3, matching what Railway's build log reports), and the exact same npm run build invocation, run locally with the same env var set, correctly bakes the value into the bundle. Code and mechanism both work locally — this is specific to the Railway build environment.
Shadow config or checked-in .env — ruled out via git ls-files; neither exists.
Logs (most recent deploy):
[stage-0 13/15] RUN --mount=type=cache,id=s/435afae8-.../node_modules/cache,target=/app/node_modules/.cache npm run build
...
vite build
vite v6.4.3 building for production...
✓ 1771 modules transformed.
dist/client/assets/index-C10gUv18.js 679.73 kB │ gzip: 177.31 kB
✓ built in 2.88s
No error — the var just silently resolves empty inside that build's Node process, despite being visible to a plain echo in the same shell one line earlier.
Question: is there a known difference between what's visible to the Nixpacks build shell (echo $VAR works) versus what the Node process spawned for vite build sees in its own process.env? Any way to get visibility into what that build's actual Node process sees?
Pinned Solution
a month ago
There should not normally be a separate Railway environment for the shell and the Node process spawned by npm.
Railway currently documents that service variables are available during the build step, and Vite gives existing VITE_* process environment variables priority when producing import.meta.env.
However, this command:
echo "GA_CHECK=$VITE_GA_MEASUREMENT_ID" && vite buildonly confirms that the shell can expand the value. It does not yet prove what the actual Node process sees, or which Vite config file is being loaded.
I would temporarily change the build command to:
node -e '
const value = process.env.VITE_GA_MEASUREMENT_ID;
console.log("NODE_GA_PRESENT=" + Boolean(value));
console.log("NODE_GA_LENGTH=" + (value?.length ?? 0));
' &&
npx vite build --config ./vite.config.ts --debugThen place an unmistakable diagnostic directly inside vite.config.ts:
import { defineConfig, loadEnv } from "vite";
console.log("LOADED_VITE_CONFIG", import.meta.url);
console.log("VITE_CONFIG_CWD", process.cwd());
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "VITE_");
console.log(
"CONFIG_GA_PRESENT",
Boolean(process.env.VITE_GA_MEASUREMENT_ID),
);
console.log(
"LOAD_ENV_GA_PRESENT",
Boolean(env.VITE_GA_MEASUREMENT_ID),
);
return {
// existing configuration
};
});This separates the possible failure points:
- If
NODE_GA_PRESENT=false, the variable is not reaching the Node build process despite the shell result. - If Node sees it but neither config diagnostic appears, Vite is not loading this config file.
- If Node and the config both see it, this is not a Railway variable-injection problem. The issue is then in the resolved Vite configuration or in how the output is being inspected.
- If
process.envsees it butloadEnvdoes not, that would be unexpected for Vite 6.4.3 and would be a useful minimal reproduction for Vite.
Because this may be a monorepo, also verify:
- Railway's configured Root Directory;
- the working directory printed by
process.cwd(); - the exact
buildscript in thepackage.jsonused by Railway; - that there is no
--configargument selecting another file; - that
vite.config.tsis present in the deployed commit; - that
npx vite build --config ./vite.config.tssucceeds from that exact directory.
Railway states that all build and deploy commands operate inside the configured Root Directory. A local build from the repository root and a Railway build from a subdirectory are therefore not necessarily equivalent.
2 Replies
a month ago
This thread has been opened as a bounty so the community can help solve it.
Status changed to Open Railway • about 1 month ago
a month ago
There should not normally be a separate Railway environment for the shell and the Node process spawned by npm.
Railway currently documents that service variables are available during the build step, and Vite gives existing VITE_* process environment variables priority when producing import.meta.env.
However, this command:
echo "GA_CHECK=$VITE_GA_MEASUREMENT_ID" && vite buildonly confirms that the shell can expand the value. It does not yet prove what the actual Node process sees, or which Vite config file is being loaded.
I would temporarily change the build command to:
node -e '
const value = process.env.VITE_GA_MEASUREMENT_ID;
console.log("NODE_GA_PRESENT=" + Boolean(value));
console.log("NODE_GA_LENGTH=" + (value?.length ?? 0));
' &&
npx vite build --config ./vite.config.ts --debugThen place an unmistakable diagnostic directly inside vite.config.ts:
import { defineConfig, loadEnv } from "vite";
console.log("LOADED_VITE_CONFIG", import.meta.url);
console.log("VITE_CONFIG_CWD", process.cwd());
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "VITE_");
console.log(
"CONFIG_GA_PRESENT",
Boolean(process.env.VITE_GA_MEASUREMENT_ID),
);
console.log(
"LOAD_ENV_GA_PRESENT",
Boolean(env.VITE_GA_MEASUREMENT_ID),
);
return {
// existing configuration
};
});This separates the possible failure points:
- If
NODE_GA_PRESENT=false, the variable is not reaching the Node build process despite the shell result. - If Node sees it but neither config diagnostic appears, Vite is not loading this config file.
- If Node and the config both see it, this is not a Railway variable-injection problem. The issue is then in the resolved Vite configuration or in how the output is being inspected.
- If
process.envsees it butloadEnvdoes not, that would be unexpected for Vite 6.4.3 and would be a useful minimal reproduction for Vite.
Because this may be a monorepo, also verify:
- Railway's configured Root Directory;
- the working directory printed by
process.cwd(); - the exact
buildscript in thepackage.jsonused by Railway; - that there is no
--configargument selecting another file; - that
vite.config.tsis present in the deployed commit; - that
npx vite build --config ./vite.config.tssucceeds from that exact directory.
Railway states that all build and deploy commands operate inside the configured Root Directory. A local build from the repository root and a Railway build from a subdirectory are therefore not necessarily equivalent.
ve-jo
There should not normally be a separate Railway environment for the shell and the Node process spawned by `npm`. Railway currently documents that service variables are available during the build step, and Vite gives existing `VITE_*` process environment variables priority when producing `import.meta.env`. However, this command: ```bash echo "GA_CHECK=$VITE_GA_MEASUREMENT_ID" && vite build ``` only confirms that the shell can expand the value. It does not yet prove what the actual Node process sees, or which Vite config file is being loaded. I would temporarily change the build command to: ```bash node -e ' const value = process.env.VITE_GA_MEASUREMENT_ID; console.log("NODE_GA_PRESENT=" + Boolean(value)); console.log("NODE_GA_LENGTH=" + (value?.length ?? 0)); ' && npx vite build --config ./vite.config.ts --debug ``` Then place an unmistakable diagnostic directly inside `vite.config.ts`: ```ts import { defineConfig, loadEnv } from "vite"; console.log("LOADED_VITE_CONFIG", import.meta.url); console.log("VITE_CONFIG_CWD", process.cwd()); export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), "VITE_"); console.log( "CONFIG_GA_PRESENT", Boolean(process.env.VITE_GA_MEASUREMENT_ID), ); console.log( "LOAD_ENV_GA_PRESENT", Boolean(env.VITE_GA_MEASUREMENT_ID), ); return { // existing configuration }; }); ``` This separates the possible failure points: - If `NODE_GA_PRESENT=false`, the variable is not reaching the Node build process despite the shell result. - If Node sees it but neither config diagnostic appears, Vite is not loading this config file. - If Node and the config both see it, this is not a Railway variable-injection problem. The issue is then in the resolved Vite configuration or in how the output is being inspected. - If `process.env` sees it but `loadEnv` does not, that would be unexpected for Vite 6.4.3 and would be a useful minimal reproduction for Vite. Because this may be a monorepo, also verify: - Railway's configured Root Directory; - the working directory printed by `process.cwd()`; - the exact `build` script in the `package.json` used by Railway; - that there is no `--config` argument selecting another file; - that `vite.config.ts` is present in the deployed commit; - that `npx vite build --config ./vite.config.ts` succeeds from that exact directory. Railway states that all build and deploy commands operate inside the configured Root Directory. A local build from the repository root and a Railway build from a subdirectory are therefore not necessarily equivalent.
a month ago
thank you, we found an even dumber (on my part) solution is that i was sending tag in array instead of object to GA4. but your answer really helped us troubleshoot that it wasn't a packaging problem. thank you!
Status changed to Solved medim • about 1 month ago