Learning objectives
After reading this article you will be able to:
- Tell build-time and runtime variables apart in Next.js, Vite and Docker
- Decide which values can be public and which must stay on the server
- Keep secrets out of bundles and Docker image layers
On this page
- What is the difference between build-time and runtime variables?
- Which variables are build-time in Next.js?
- Which variables are build-time in Vite and other bundlers?
- How do build and runtime variables work in Docker?
- How do you decide where a value belongs?
- Why doesn't a changed variable take effect?
- How to do this on Square Cloud
- FAQs
You set API_URL in your dashboard, restart the app, and the site still calls the old address. Or a secret you thought was private turns up in the browser's network tab. Both bugs share one cause: the variable was read at a different moment than you assumed.
A variable can be consumed at two moments. At build time, a tool such as Next.js, Vite or Docker reads it while producing the output. At runtime, the running process reads it from its environment. When the read happens decides whether a change needs a restart or a rebuild, and whether a value can leak.
What is the difference between build-time and runtime variables?#
A build-time variable is read once, during the build, and usually written into the output as plain text. After that, the file is the source of truth, not the environment.
A runtime variable is looked up every time the process starts, through process.env in Node.js or os.environ in Python. Change the value, restart, and the app sees the new one. This is the model of the twelve-factor config rule: config lives in environment variables, because config varies between deploys and code doesn't.
| Build time | Runtime | |
|---|---|---|
| Read by | Bundler, compiler, Docker build | Your running process |
| Change needs | Rebuild | Restart |
| Where the value ends up | Inside the output files | Only in process memory |
| Safe for secrets | Only if it never reaches the output | Yes, if you never log or expose it |
Which variables are build-time in Next.js?#
In Next.js, anything prefixed with NEXT_PUBLIC_ is build time. The docs say Next.js inlines the value into the JavaScript bundle during next build, replacing each process.env.NEXT_PUBLIC_* reference with a hard-coded string. After the build, the app no longer responds to changes in those variables, and a single Docker image promoted to several environments keeps the value from the build.
Unprefixed variables stay on the server. Read them during dynamic rendering and they are evaluated at runtime:
// app/page.tsx
import { connection } from "next/server";
export default async function Page() {
await connection(); // opt into dynamic rendering: evaluated per request
const apiUrl = process.env.API_URL; // runtime, server only
return <p>Using {apiUrl}</p>;
}Dynamic lookups such as process.env[name] are not inlined, so they never reach the browser. If the browser needs a runtime value, expose it from your own API route instead of a NEXT_PUBLIC_ variable.
Which variables are build-time in Vite and other bundlers?#
Vite replaces import.meta.env values statically at build time. Only variables starting with VITE_ reach client code, and the docs warn they should not contain sensitive information such as API keys, because they are bundled into the source.
// src/main.js
const api = import.meta.env.VITE_API_URL; // frozen into the bundle at build
const dbPassword = import.meta.env.DB_PASSWORD; // undefined in client codeVite also gives variables already set in the environment priority over .env files, so VITE_API_URL=https://api.example.com npm run build wins over a .env file. Other bundlers use the same idea with their own prefix: a prefix means "copy this into the browser bundle at build time".
A plain Node.js or Python server has no build step, so everything it reads is runtime.
How do build and runtime variables work in Docker?#
Docker adds a third question: does the value end up in an image layer? ARG exists during the build and ENV persists in the image, but Docker's documentation says build arguments and environment variables should not carry secrets because they stay in the final image.
For a secret the build truly needs (a private package registry token, for example), use a secret mount. It is mounted as a file at /run/secrets/<id> (or exposed as an environment variable with env=) for that single instruction only and is not saved in a layer:
#syntax=docker/dockerfile:1
FROM node:lts-slim AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN npm ci
COPY . .
ARG VITE_API_URL
RUN npm run build
FROM node:lts-slim
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]Here npm_token exists only during that one RUN (your .npmrc must read ${NPM_TOKEN}), VITE_API_URL is a public value baked in on purpose, and DATABASE_URL is deliberately absent from the image: it is passed when the container starts.
docker build --secret id=npm_token,env=NPM_TOKEN \
--build-arg VITE_API_URL=https://api.example.com -t my-app .
docker run -e DATABASE_URL="$DATABASE_URL" my-appThe multi-stage layout also helps: the final image copies only the files you choose from the build stage, not its layers or build arguments. More on this in how to dockerize an app.
How do you decide where a value belongs?#
Ask three questions, in order:
- Would it hurt if any visitor saw it? If yes (database URL, API secret, bot token), it must be a runtime, server-only variable. Never prefix it.
- Does the browser need it? Then it is public by definition. A build-time prefix is fine for values like an analytics ID or a public API address.
- Do you need to change it without rebuilding? Then don't inline it. Read it on the server at startup, or serve it from a small config endpoint.
A frequent trap: a secret works locally because your .env holds it, then leaks because someone renamed it VITE_SECRET to make an undefined error go away. If a framework says a variable is undefined in the browser, that is the protection working.
Why doesn't a changed variable take effect?#
Work through this checklist:
- Process started before the change. Runtime variables are read at startup; restart the app.
- Value inlined at build. Search the build output with
grep -r "old-value" .next dist build. If it appears, rebuild. - The build ran without the variable. Check that it exists where the build runs, which may differ from where the app runs. See how to read build errors and deploying with GitHub Actions, where build variables live in the CI settings.
- Everything is a string.
"false"is truthy in a condition; convert it in code.
How to do this on Square Cloud#
On Square Cloud, you set environment variables per application in the dashboard, API, CLI or VS Code extension. The dashboard also imports a .env file and exports KEY=VALUE. A change applies on the next restart, with no rebuild, which matches the runtime model above. Your app can read SQUARECLOUD_APP_ID at runtime to get its own application ID.
So secrets such as database URLs and tokens belong in these runtime variables, not in your repository or bundle. There is no build step on the platform: on start it only installs dependencies (npm install when there is no node_modules), and your variables already exist at that point. Values your bundler inlines (NEXT_PUBLIC_, VITE_) have to be right in the CI job where you run the build, such as GitHub Actions, or you can put the build in the start command, where the variables are already set.
Details are in the environment variables guide.
Deploy
Push from GitHub, the CLI or a ZIP and go live in seconds
FAQs
Why does my variable show the old value after I changed it in the dashboard?
If the framework inlined it at build time (NEXT_PUBLIC_ in Next.js, VITE_ in Vite), the old value is already written into the JavaScript files. Changing the environment does nothing until you rebuild.
Can I put an API secret in a NEXT_PUBLIC_ or VITE_ variable?
No. Those variables are copied into the JavaScript sent to every visitor, and anyone can read them in the browser. Keep secrets in unprefixed variables read only on the server.
Is Docker ARG safe for passwords?
No. Docker documents that build arguments and environment variables stay in the final image. Use a secret mount (RUN --mount=type=secret) for build-time secrets and pass runtime secrets when the container starts.
Do plain Node.js and Python apps have build-time variables?
Only if you add a build step. An interpreted app with no bundler reads process.env or os.environ when it starts, so every variable is a runtime variable.
More in Deploy
See all 33 articles- What are environment variables?Environment variables are key-value pairs the operating system hands to a process at startup. Learn to read them in Node.js and Python and keep tokens out of code.
- What is a deployment?A deployment takes a version of your code to a server, prepares it and starts it. See the stages, the methods and how to roll back when it goes wrong.
- How much RAM and CPU does my app need?Size RAM and CPU by measuring your app's real peak and adding about 30% headroom. Learn how to measure in Node.js, Python and Java and what to do if short.
- Automatic deploys with GitHub ActionsAutomatic deploys with GitHub Actions ship your code to the server on every push. See the workflow step by step, secrets, tests before deploy and common errors.