Variáveis de ambiente: tempo de build vs tempo de execução

Variáveis de build são lidas durante o build e copiadas para os arquivos gerados. Variáveis de runtime são lidas do ambiente a cada início do app, então mudá-las pede um restart, não um novo build.

Atualizado em 6 min de leitura

Objetivos de aprendizado

Depois de ler este artigo, você vai conseguir:

  • Diferenciar variáveis de build e de runtime no Next.js, no Vite e no Docker
  • Decidir quais valores podem ser públicos e quais precisam ficar no servidor
  • Manter segredos fora dos bundles e das camadas de imagem Docker
Nesta página

Você define API_URL no painel, reinicia o app e o site continua chamando o endereço antigo. Ou um segredo que você achava privado aparece na aba de rede do navegador. Os dois problemas têm a mesma causa: a variável foi lida num momento diferente do que você imaginava.

Uma variável pode ser consumida em dois momentos. No tempo de build, uma ferramenta como Next.js, Vite ou Docker lê o valor enquanto gera os arquivos finais. No tempo de execução (runtime), o processo em execução lê o valor do seu ambiente. O momento da leitura decide se uma mudança pede restart ou novo build, e se o valor pode vazar.

Tempo de buildVariáveisNEXT_PUBLIC_, VITE_Bundlevalor gravadoNovo valorpede novo buildTempo de execuçãoVariáveisprocess.envProcessolê ao iniciarNovo valorvale no restart
Variável de build fica gravada no bundle até o próximo build; variável de runtime é lida de novo a cada início.

Qual a diferença entre variáveis de build e de runtime?#

Uma variável de build é lida uma vez, durante o build, e em geral escrita nos arquivos gerados como texto puro. Depois disso, quem manda é o arquivo, não o ambiente.

Uma variável de runtime é consultada toda vez que o processo inicia, por process.env no Node.js ou os.environ no Python. Você muda o valor, reinicia e o app enxerga o novo. É o modelo da regra de configuração do twelve-factor: a configuração mora em variáveis de ambiente, porque ela muda entre deploys e o código não.

Tempo de build Runtime
Quem lê Bundler, compilador, build do Docker O seu processo em execução
Mudança exige Novo build Restart
Onde o valor termina Dentro dos arquivos gerados Só na memória do processo
Seguro para segredos Só se nunca chegar aos arquivos gerados Sim, se você nunca o registrar em log nem o expuser

Quais variáveis são de build no Next.js?#

No Next.js, tudo que começa com NEXT_PUBLIC_ é de build. A documentação diz que o Next.js embute o valor no bundle JavaScript durante o next build, trocando cada referência process.env.NEXT_PUBLIC_* por uma string fixa. Depois do build, o app não reage mais a mudanças nessas variáveis, e uma mesma imagem Docker promovida para vários ambientes mantém o valor do build.

Variáveis sem prefixo ficam no servidor. Se você as lê durante a renderização dinâmica, elas são avaliadas em runtime:

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

export default async function Page() {
  await connection(); // ativa a renderização dinâmica: avaliado a cada requisição
  const apiUrl = process.env.API_URL; // runtime, só no servidor
  return <p>Using {apiUrl}</p>;
}

Leituras dinâmicas como process.env[name] não são embutidas, então nunca chegam ao navegador. Se o navegador precisa de um valor de runtime, exponha-o por uma rota de API sua em vez de uma variável NEXT_PUBLIC_.

Quais variáveis são de build no Vite e em outros bundlers?#

O Vite substitui os valores de import.meta.env de forma estática no build. Só variáveis que começam com VITE_ chegam ao código do cliente, e a documentação avisa que elas não devem conter informação sensível, como chaves de API, porque vão para dentro do código empacotado.

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

O Vite também dá prioridade às variáveis que já existem no ambiente sobre os arquivos .env, então VITE_API_URL=https://api.example.com npm run build vence um arquivo .env. Outros bundlers seguem a mesma ideia com o próprio prefixo: o prefixo significa "copie isto para o bundle do navegador no build".

Um servidor Node.js ou Python simples não tem etapa de build, então tudo que ele lê é runtime.

Como variáveis de build e de runtime funcionam no Docker?#

O Docker acrescenta uma terceira pergunta: o valor termina numa camada da imagem? ARG existe durante o build e ENV persiste na imagem, mas a documentação do Docker diz que argumentos de build e variáveis de ambiente não devem carregar segredos, porque ficam na imagem final.

Para um segredo de que o build realmente precisa (um token de registro privado de pacotes, por exemplo), use um secret mount. Ele é montado como arquivo em /run/secrets/<id> (ou exposto como variável de ambiente com env=) só para aquela instrução e não fica salvo em nenhuma camada:

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

Aqui o npm_token existe só durante aquele RUN (o seu .npmrc precisa ler ${NPM_TOKEN}), VITE_API_URL é um valor público embutido de propósito e DATABASE_URL fica fora da imagem de propósito: ela é passada quando o container 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

O build em múltiplos estágios também ajuda: a imagem final copia só os arquivos que você escolhe do estágio de build, não as camadas nem os argumentos dele. Veja mais em como dockerizar uma aplicação.

Como decidir onde cada valor fica?#

Faça três perguntas, nesta ordem:

  1. Faria mal se qualquer visitante visse? Se sim (URL do banco, segredo de API, token de bot), precisa ser uma variável de runtime, só no servidor. Nunca coloque prefixo.
  2. O navegador precisa dela? Então ela é pública por definição. Um prefixo de build serve para valores como um ID de analytics ou o endereço de uma API pública.
  3. Você precisa mudá-la sem novo build? Então não a embuta. Leia no servidor ao iniciar, ou sirva por um pequeno endpoint de configuração.

Uma armadilha frequente: um segredo funciona na sua máquina porque o seu .env o contém, e depois vaza porque alguém o renomeou para VITE_SECRET só para fazer sumir um erro de undefined. Se o framework diz que uma variável é undefined no navegador, é a proteção funcionando.

Por que uma variável alterada não faz efeito?#

Siga esta lista:

  • O processo iniciou antes da mudança. Variáveis de runtime são lidas no início; reinicie o app.
  • O valor foi embutido no build. Procure nos arquivos gerados com grep -r "old-value" .next dist build. Se aparecer, gere um novo build.
  • O build rodou sem a variável. Confira se ela existe onde o build roda, que pode ser outro lugar além de onde o app roda. Veja como ler erros de build e deploy com GitHub Actions, onde as variáveis de build ficam nas configurações do CI.
  • Tudo é string. "false" é verdadeiro numa condição; converta o valor no código.

Como fazer isso na Square Cloud#

Na Square Cloud, você define as variáveis de ambiente por aplicação no painel, na API, na CLI ou na extensão do VS Code. O painel também importa um arquivo .env e exporta no formato KEY=VALUE. A mudança vale no próximo restart, sem novo build, o que bate com o modelo de runtime descrito acima. O seu app pode ler SQUARECLOUD_APP_ID em runtime para saber o ID da própria aplicação.

Por isso, segredos como URLs de banco e tokens ficam nessas variáveis de runtime, não no repositório nem no bundle. Não há etapa de build na plataforma: no início ela só instala as dependências (npm install quando não existe node_modules), e as suas variáveis já existem nesse momento. Os valores que o seu bundler embute (NEXT_PUBLIC_, VITE_) precisam estar certos no job de CI onde você roda o build, como o GitHub Actions, ou você pode colocar o build no comando de início, onde as variáveis já estão definidas.

Os detalhes estão no guia de variáveis de ambiente.

Deploy

Envie pelo GitHub, pela CLI ou em ZIP e publique em segundos

Saiba mais

Perguntas frequentes

Por que minha variável mostra o valor antigo depois que eu mudei no painel?

Se o framework embutiu a variável no build (NEXT_PUBLIC_ no Next.js, VITE_ no Vite), o valor antigo já está escrito nos arquivos JavaScript. Mudar o ambiente não adianta até você gerar um novo build.

Posso colocar um segredo de API numa variável NEXT_PUBLIC_ ou VITE_?

Não. Essas variáveis são copiadas para o JavaScript enviado a todo visitante, e qualquer pessoa consegue lê-las no navegador. Guarde segredos em variáveis sem prefixo, lidas só no servidor.

ARG do Docker é seguro para senhas?

Não. A documentação do Docker avisa que argumentos de build e variáveis de ambiente ficam na imagem final. Use um secret mount (RUN --mount=type=secret) para segredos do build e passe os segredos de runtime quando o container iniciar.

Apps Node.js e Python sem bundler têm variáveis de build?

Só se você adicionar uma etapa de build. Um app interpretado sem bundler lê process.env ou os.environ quando inicia, então toda variável é de runtime.

Próximo artigoComo fazer deploy de um monorepoFazer deploy de um monorepo é publicar um app de um repositório com vários. Veja como podar dependências e empacotar um pacote com pnpm e Turborepo.