Variables de entorno en tiempo de build vs en tiempo de ejecución

Las variables de build se leen mientras se construye el proyecto y se copian en los archivos generados. Las de runtime se leen del entorno cada vez que la app inicia, así que cambiarlas pide un reinicio, no un nuevo build.

Actualizado el 6 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Distinguir variables de build y de runtime en Next.js, Vite y Docker
  • Decidir qué valores pueden ser públicos y cuáles deben quedarse en el servidor
  • Mantener los secretos fuera de los bundles y de las capas de las imágenes Docker
En esta página

Defines API_URL en el panel, reinicias la app y el sitio sigue llamando a la dirección vieja. O un secreto que creías privado aparece en la pestaña de red del navegador. Los dos problemas tienen la misma causa: la variable se leyó en un momento distinto del que suponías.

Una variable se puede consumir en dos momentos. En tiempo de build, una herramienta como Next.js, Vite o Docker la lee mientras genera los archivos finales. En tiempo de ejecución (runtime), el proceso en marcha la lee de su entorno. El momento de la lectura decide si un cambio pide un reinicio o un nuevo build, y si el valor puede filtrarse.

Tiempo de buildVariablesNEXT_PUBLIC_, VITE_Bundlevalor grabadoNuevo valorpide nuevo buildTiempo de ejecuciónVariablesprocess.envProcesolee al iniciarNuevo valorcon un reinicio
Una variable de build queda escrita en el bundle hasta el próximo build; una de runtime se vuelve a leer en cada inicio.

¿Qué diferencia hay entre variables de build y de runtime?#

Una variable de build se lee una sola vez, durante el build, y por lo general se escribe en los archivos generados como texto plano. A partir de ahí, manda el archivo, no el entorno.

Una variable de runtime se consulta cada vez que el proceso inicia, con process.env en Node.js u os.environ en Python. Cambias el valor, reinicias y la app ve el nuevo. Es el modelo de la regla de configuración de twelve-factor: la configuración vive en variables de entorno, porque cambia entre deploys y el código no.

Tiempo de build Runtime
Quién la lee Bundler, compilador, build de Docker Tu proceso en ejecución
Un cambio exige Nuevo build Reinicio
Dónde termina el valor Dentro de los archivos generados Solo en la memoria del proceso
Segura para secretos Solo si nunca llega a los archivos generados Sí, si nunca la registras en logs ni la expones

¿Qué variables son de build en Next.js?#

En Next.js, todo lo que empieza con NEXT_PUBLIC_ es de build. La documentación dice que Next.js incrusta el valor en el bundle de JavaScript durante next build, reemplazando cada referencia process.env.NEXT_PUBLIC_* por un string fijo. Después del build, la app ya no responde a cambios en esas variables, y una misma imagen Docker promovida a varios entornos conserva el valor del build.

Las variables sin prefijo se quedan en el servidor. Si las lees durante el renderizado dinámico, se evalúan en runtime:

tsx
// app/page.tsx
import { connection } from "next/server";

export default async function Page() {
  await connection(); // activa el renderizado dinámico: se evalúa en cada petición
  const apiUrl = process.env.API_URL; // runtime, solo en el servidor
  return <p>Using {apiUrl}</p>;
}

Las lecturas dinámicas como process.env[name] no se incrustan, así que nunca llegan al navegador. Si el navegador necesita un valor de runtime, exponlo desde una ruta de API propia en lugar de una variable NEXT_PUBLIC_.

¿Qué variables son de build en Vite y otros bundlers?#

Vite reemplaza los valores de import.meta.env de forma estática en el build. Solo las variables que empiezan con VITE_ llegan al código del cliente, y la documentación advierte que no deben contener información sensible, como claves de API, porque quedan dentro del código empaquetado.

js
// src/main.js
const api = import.meta.env.VITE_API_URL; // congelada en el bundle durante el build
const dbPassword = import.meta.env.DB_PASSWORD; // undefined en el código del cliente

Vite también da prioridad a las variables que ya existen en el entorno sobre los archivos .env, así que VITE_API_URL=https://api.example.com npm run build le gana a un archivo .env. Otros bundlers siguen la misma idea con su propio prefijo: el prefijo significa "copia esto en el bundle del navegador durante el build".

Un servidor Node.js o Python simple no tiene paso de build, así que todo lo que lee es runtime.

¿Cómo funcionan las variables de build y de runtime en Docker?#

Docker suma una tercera pregunta: ¿el valor termina en una capa de la imagen? ARG existe durante el build y ENV persiste en la imagen, pero la documentación de Docker dice que los argumentos de build y las variables de entorno no deben llevar secretos, porque quedan en la imagen final.

Para un secreto que el build de verdad necesita (un token de un registro privado de paquetes, por ejemplo), usa un secret mount. Se monta como archivo en /run/secrets/<id> (o se expone como variable de entorno con env=) solo para esa instrucción y no se guarda en ninguna capa:

dockerfile
#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"]

Aquí npm_token existe solo durante ese RUN (tu .npmrc debe leer ${NPM_TOKEN}), VITE_API_URL es un valor público incrustado a propósito y DATABASE_URL queda fuera de la imagen a propósito: se pasa cuando el contenedor inicia.

bash
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-app

El build en varias etapas también ayuda: la imagen final copia solo los archivos que eliges de la etapa de build, no sus capas ni sus argumentos. Más sobre esto en cómo dockerizar una aplicación.

¿Cómo decidir dónde va cada valor?#

Hazte tres preguntas, en este orden:

  1. ¿Haría daño que cualquier visitante lo viera? Si la respuesta es sí (URL de la base de datos, secreto de API, token de bot), debe ser una variable de runtime, solo en el servidor. Nunca le pongas prefijo.
  2. ¿El navegador la necesita? Entonces es pública por definición. Un prefijo de build sirve para valores como un ID de analítica o la dirección de una API pública.
  3. ¿Necesitas cambiarla sin un nuevo build? Entonces no la incrustes. Léela en el servidor al iniciar, o sírvela desde un pequeño endpoint de configuración.

Una trampa frecuente: un secreto funciona en tu máquina porque tu .env lo tiene, y luego se filtra porque alguien lo renombró a VITE_SECRET para que desapareciera un error de undefined. Si el framework dice que una variable es undefined en el navegador, es la protección haciendo su trabajo.

¿Por qué una variable cambiada no surte efecto?#

Revisa esta lista:

  • El proceso inició antes del cambio. Las variables de runtime se leen al iniciar; reinicia la app.
  • El valor se incrustó en el build. Búscalo en los archivos generados con grep -r "old-value" .next dist build. Si aparece, haz un nuevo build.
  • El build corrió sin la variable. Comprueba que exista donde corre el build, que puede ser un lugar distinto de donde corre la app. Mira cómo leer errores de build y deploy con GitHub Actions, donde las variables de build viven en la configuración del CI.
  • Todo es un string. "false" es verdadero en una condición; conviértelo en el código.

Cómo hacerlo en Square Cloud#

En Square Cloud, defines las variables de entorno por aplicación en el panel, la API, la CLI o la extensión de VS Code. El panel también importa un archivo .env y exporta en formato KEY=VALUE. El cambio se aplica en el próximo reinicio, sin nuevo build, lo que coincide con el modelo de runtime de arriba. Tu app puede leer SQUARECLOUD_APP_ID en runtime para conocer el ID de su propia aplicación.

Por eso, los secretos como URLs de bases de datos y tokens van en estas variables de runtime, no en tu repositorio ni en tu bundle. La plataforma no tiene paso de build: al iniciar solo instala las dependencias (npm install cuando no existe node_modules), y tus variables ya existen en ese momento. Los valores que tu bundler incrusta (NEXT_PUBLIC_, VITE_) tienen que estar bien en el job de CI donde corres el build, como GitHub Actions, o puedes poner el build en el comando de inicio, donde las variables ya están definidas.

Los detalles están en la guía de variables de entorno.

Deploy

Sube desde GitHub, la CLI o un ZIP y publica en segundos

Más información

Preguntas frecuentes

¿Por qué mi variable muestra el valor viejo después de cambiarla en el panel?

Si el framework la incrustó en el build (NEXT_PUBLIC_ en Next.js, VITE_ en Vite), el valor viejo ya está escrito en los archivos JavaScript. Cambiar el entorno no sirve hasta que hagas un nuevo build.

¿Puedo poner un secreto de API en una variable NEXT_PUBLIC_ o VITE_?

No. Esas variables se copian en el JavaScript que se envía a cada visitante, y cualquiera puede leerlas en el navegador. Guarda los secretos en variables sin prefijo, leídas solo en el servidor.

¿ARG de Docker es seguro para contraseñas?

No. La documentación de Docker advierte que los argumentos de build y las variables de entorno quedan en la imagen final. Usa un secret mount (RUN --mount=type=secret) para los secretos del build y pasa los secretos de runtime al iniciar el contenedor.

¿Las apps Node.js y Python sin bundler tienen variables de build?

Solo si agregas un paso de build. Una app interpretada sin bundler lee process.env u os.environ al iniciar, así que toda variable es de runtime.

Siguiente artículoCómo desplegar un monorepoDesplegar un monorepo es publicar una app de un repositorio que tiene varias. Aprende a podar dependencias y empaquetar un paquete con pnpm y Turborepo.

Más en Deploy

Ver los 33 artículos