Variáveis de Ambiente no Node.js: do .env ao Deploy Seguro
← Voltar para Codeshort

Variáveis de Ambiente no Node.js: do .env ao Deploy Seguro

Aprenda a estruturar variáveis de ambiente no Node.js da forma certa — do arquivo .env local até o deploy em produção sem expor segredos.

DC
Dev Code Software
08 de maio de 2026·10 min de leitura

Variáveis de Ambiente no Node.js: do .env ao Deploy Seguro

Você já commitou um .env com credencial real? Não precisa responder — a maioria dos desenvolvedores Node.js fez isso pelo menos uma vez. O problema quase nunca é descuido: é ausência de estrutura desde o início do projeto.

Este guia cobre tudo que você precisa saber para lidar com variáveis de ambiente de forma profissional — do desenvolvimento local até o deploy em produção, passando por validação tipada com Zod e estratégias para não depender de arquivo .env em servidor nenhum.


Sumário


Como o Node.js lida com variáveis de ambiente

O Node.js expõe variáveis de ambiente através do objeto global process.env. Qualquer variável definida no sistema operacional antes de o processo iniciar fica acessível ali:

DATABASE_URL=postgres://localhost/mydb node index.js
console.log(process.env.DATABASE_URL)
// postgres://localhost/mydb

Esse mecanismo existe desde sempre e é a base de tudo que vem a seguir. O problema nunca foi o mecanismo — foi a falta de disciplina em volta dele.

Node.js 20.6+: carregamento nativo sem biblioteca

A partir do Node.js 20.6, você pode carregar um arquivo .env diretamente via flag, sem instalar nenhuma dependência:

node --env-file=.env index.js

Para projetos novos e simples, isso resolve. Mas o dotenv ainda tem vantagens em cenários mais complexos — veja a seção a seguir.


dotenv: instalação e a armadilha da ordem de importação

O dotenv continua sendo o padrão mais usado no ecossistema Node.js. A instalação é trivial:

npm install dotenv

O ponto onde a maioria erra não é a instalação — é quando dotenv.config() é chamado.

O erro clássico com CommonJS

// ❌ Isso quebra silenciosamente
const express = require('express')
const { connectDB } = require('./database')
const dotenv = require('dotenv')
 
dotenv.config() // tarde demais — connectDB já tentou usar process.env
// ✓ dotenv.config() deve ser executado antes de qualquer import que use process.env
require('dotenv').config()
 
const express = require('express')
const { connectDB } = require('./database')

O problema fica pior com ES Modules

Com "type": "module" no package.json, declarações import são hoisted — executadas antes do código do módulo. Isso significa que mesmo colocar dotenv.config() na primeira linha do arquivo não garante que ele rode antes de outros módulos serem carregados.

A solução limpa é um arquivo de entrada dedicado:

// src/env.js — carrega as variáveis antes de qualquer outra coisa
import dotenv from 'dotenv'
dotenv.config()
// index.js — env.js SEMPRE primeiro
import './src/env.js'
import express from 'express'
import { connectDB } from './database.js'

Atenção: Um connectDB chamado antes do dotenv.config() vai tentar conectar com undefined como string de conexão. Dependendo do driver de banco, isso gera um erro genérico que pode levar horas para rastrear — não um erro claro do tipo "DATABASE_URL is undefined".

dotenv ainda faz sentido com Node 20+?

Depende do projeto. A flag --env-file nativa cobre o básico. O dotenv ainda agrega em:

  • dotenv-expand — interpolação de variáveis (DATABASE_URL=${DB_HOST}:5432/mydb)
  • Múltiplos arquivosdotenv.config({ path: '.env.local' })
  • Integração com ferramentas de build — webpack, Jest, Next.js esperam o dotenv Para projetos novos sem essas necessidades, a flag nativa é suficiente.

Estrutura de arquivos .env para múltiplos ambientes

Essa é a estrutura que elimina a maioria dos vazamentos acidentais:

.env                  ← NÃO commitado, valores locais reais
.env.example          ← Commitado, sem valores, só as chaves
.env.test             ← Commitado, valores fake para CI/CD
.env.production       ← NUNCA commitado, gerenciado externamente

O .env.example é o contrato da aplicação

Todo desenvolvedor que clonar o repositório precisa saber exatamente quais variáveis configurar. O .env.example serve isso:

# .env.example — commite este arquivo, sem valores reais
DATABASE_URL=
JWT_SECRET=
REDIS_URL=
STRIPE_SECRET_KEY=
NODE_ENV=development
PORT=3000

Sem valores. Só a estrutura. Qualquer variável nova adicionada ao projeto deve aparecer aqui primeiro.

.gitignore precisa ser explícito

# .gitignore
.env
.env.local
.env.production
.env.*.local

Não use só .env — uma hora alguém cria .env.production.local e commita sem perceber.


Validação na inicialização com Zod

Esse foi o problema que apareceu num code review real: "e se JWT_SECRET estiver undefined em produção?" A resposta era um crash no momento do login. Boa hora para descobrir em review, péssima às 2h da manhã com usuários acordados.

A solução é validar todas as variáveis obrigatórias antes de a aplicação subir — não no runtime, quando o dano já está feito.

Opção 1 — Validação manual, sem dependência

// src/config.js
const required = ['DATABASE_URL', 'JWT_SECRET', 'REDIS_URL']
 
const missing = required.filter(key => !process.env[key])
 
if (missing.length > 0) {
  console.error(`[config] Variáveis obrigatórias ausentes: ${missing.join(', ')}`)
  process.exit(1)
}
 
export const config = {
  databaseUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET,
  redisUrl: process.env.REDIS_URL,
  port: Number(process.env.PORT) || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
}

Funciona, é zero dependência, mas não valida tipos nem formatos.

Opção 2 — Zod (recomendado para projetos em crescimento)

// src/config.js
import { z } from 'zod'
 
const envSchema = z.object({
  DATABASE_URL: z.string().url('DATABASE_URL deve ser uma URL válida'),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET precisa ter ao menos 32 caracteres'),
  REDIS_URL: z.string().url().optional(),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
})
 
const parsed = envSchema.safeParse(process.env)
 
if (!parsed.success) {
  console.error('[config] Variáveis de ambiente inválidas:')
  console.error(parsed.error.flatten().fieldErrors)
  process.exit(1)
}
 
export const config = parsed.data

O Zod agrega três coisas aqui que a validação manual não dá:

Coerção de tiposPORT vira número automaticamente. Chega de Number(process.env.PORT) em dez lugares diferentes.

Mensagens de erro específicas — em vez de "variável ausente", você recebe "JWT_SECRET precisa ter ao menos 32 caracteres". Isso economiza um ciclo de debug completo.

Tipagem automáticaconfig.port tem tipo number, não string | undefined. Seu editor vai reclamar antes do deploy.

Regra de ouro: Exporte sempre um objeto config tipado, nunca acesse process.env diretamente no resto da aplicação. Centralizar aqui significa que em testes você mocka um objeto, não o process.env global — o que é infinitamente menos frágil.


Variáveis de ambiente em produção

Em produção, o arquivo .env não deveria existir. As plataformas modernas injetam variáveis diretamente no processo:

PlataformaOnde configurar
RailwayDashboard → Variables
RenderDashboard → Environment
Fly.iofly secrets set CHAVE=valor
VercelDashboard → Settings → Environment Variables
AWS ECSTask Definition → Environment
KubernetesSecret + ConfigMap → envFrom

Para segredos sensíveis de verdade: cofres dedicados

Chaves de banco de dados, tokens de terceiros e certificados merecem mais do que uma variável de ambiente no dashboard de um serviço. O padrão de mercado são ferramentas de secrets management:

AWS Secrets Manager — integração nativa com EC2, ECS, Lambda. Rotação automática para RDS. Custo: ~$0,40/segredo/mês.

HashiCorp Vault — self-hosted ou cloud, controle total. Mais complexo, mais poder: audit logs, políticas granulares, leasing de credenciais.

Doppler — o mais acessível para times pequenos. Você define os segredos na interface e injeta no processo com:

doppler run -- node index.js

Nenhum arquivo .env em lugar nenhum. Rotação de segredos sem redeploy. O plano gratuito cobre a maioria dos projetos pequenos.


Erros clássicos em PRs — e como evitá-los

1. Logar process.env inteiro para debug

// ❌ Isso vai para os logs de produção — incluindo DATABASE_URL, JWT_SECRET e tudo mais
console.log('Iniciando com config:', process.env)
 
// ✓ Loga só o que faz sentido monitorar
console.log(`Servidor iniciado | porta=${config.port} | env=${config.nodeEnv}`)

Se você tem agregação de logs (Datadog, Papertrail, CloudWatch), aquele console.log(process.env) vai aparecer em texto claro em algum painel. Já aconteceu.

2. Copiar .env.production para desenvolvimento "só pra testar"

# ❌ Parece inocente na hora
cp .env.production .env

Em três meses, aquele desenvolvedor vai commitar uma migration que roda contra o banco de produção. Os dados vão embora antes de alguém perceber.

3. Comparar boolean com variável de ambiente

// ❌ Isso é sempre true — a string "false" é truthy em JavaScript
if (process.env.FEATURE_FLAG) {
  ativarFeature()
}
 
// ✓ Compare explicitamente
if (process.env.FEATURE_FLAG === 'true') {
  ativarFeature()
}

Esse bug é particularmente traiçoeiro porque .env com FEATURE_FLAG=false faz a feature ligar, não desligar.

4. Não versionar o .env.example

Novo desenvolvedor entra no projeto. Clona o repositório, roda npm install, tenta npm start e recebe um erro críptico de conexão. Quarenta minutos depois descobre que precisava criar um .env com dez variáveis que ninguém documentou.

O .env.example existe para eliminar esse onboarding doloroso. Sem ele, você está terceirizando conhecimento crítico para memória humana.


FAQ

Posso commitar o .env.test com valores reais?

Só se forem valores que não dão acesso a nada real: banco de dados local, chaves de sandbox de processadores de pagamento, tokens de ambiente de teste isolado. Nunca commite uma chave que funcione em produção, mesmo que seja "só para CI". As pipelines de CI também têm histórico de acesso.

Como uso variáveis de ambiente em monorepos?

Cada serviço deve ter seu próprio .env e seu próprio config.js. Variáveis verdadeiramente compartilhadas (como NODE_ENV) podem viver em um .env na raiz, mas carregadas explicitamente por quem precisa — nunca automaticamente. Acoplamento implícito em monorepo é receita para debug doloroso às sextas-feiras.

E no frontend? Posso usar process.env?

No frontend empacotado com Vite ou Next.js, variáveis precisam de prefixo específico (VITE_ ou NEXT_PUBLIC_) para serem incluídas no bundle. Qualquer variável sem prefixo não vai estar disponível no browser — e isso é uma decisão correta de design. Segredos não pertencem ao frontend. Nunca.

Como rotaciono uma chave que foi exposta acidentalmente?

Imediatamente, nesta ordem: revogue a chave no provedor (AWS, Stripe, GitHub, o que for), gere uma nova, atualize nas plataformas de deploy, e audite os logs para ver se a chave foi usada por alguém além de você nas últimas horas. Se o segredo estava em um commit no Git, considere o histórico comprometido — não basta deletar o arquivo ou fazer um novo commit. Use git filter-repo para reescrever o histórico e force-push. Avise sua equipe antes.

dotenv ainda faz sentido com Node.js 20+?

Para projetos simples, a flag --env-file nativa resolve. Para projetos com múltiplos ambientes, interpolação de variáveis ou integração com ferramentas de build, o dotenv ainda vale.


Próximos passos

Se sua aplicação ainda não tem essa estrutura, aqui está a ordem que faz mais sentido:

1. Crie o .env.example agora, com todas as chaves que a aplicação usa sem valores reais. Commite.

2. Revise o .gitignore para cobrir .env, .env.local, .env.production e .env.*.local.

3. Crie um src/config.js com validação na inicialização — manual ou com Zod. Substitua todos os process.env.CHAVE espalhados pelo código por config.chave.

4. Em produção, configure as variáveis diretamente na plataforma de deploy. Sem arquivos.

5. Para segredos críticos, avalie Doppler ou AWS Secrets Manager dependendo do tamanho do time e do orçamento.

O objetivo não é paranoia. É tornar o vazamento de segredos o caminho mais difícil, não o mais fácil.