- Por que segredos vazam mesmo com .env
- Como o Node.js lê variáveis de ambiente
- dotenv na ordem certa
- O que commitar — e o que nunca commitar
- Validação na inicialização: fail fast ou fail tarde?
- Separando ambientes sem bagunça
- Segredos em produção: comparando as alternativas reais
- Se o .env já foi commitado: o que fazer agora
- FAQ
- Próximos passos
Você commita um .env sem querer. O GitHub manda um alerta em menos de 60 segundos: chave da AWS detectada no repositório. Você passa a hora seguinte revogando credenciais, auditando logs de acesso e torcendo para ninguém ter automatizado um scraper nesse intervalo. Esse cenário tem nome: secret sprawl. E acontece com devs experientes, não só com quem está começando.
Por que segredos vazam mesmo com .env
O .env no .gitignore resolve apenas o caso mais óbvio. Os vazamentos reais costumam vir de outro lugar:
.envcommitado antes de entrar no.gitignore— o arquivo fica no histórico para sempre, mesmo depois de removido- Logs de deploy que printam
process.envcompleto por descuido de debug - Repositório privado que vira público durante uma migração ou mudança de plano
.env.examplecom valores reais de desenvolvimento que alguém copia direto
A raiz do problema não é o arquivo. É tratar segredo como configuração. Configuração pode ir para o repo. Segredo não pode — nunca, em nenhum ambiente.
Como o Node.js lê variáveis de ambiente
O process.env é um objeto que reflete as variáveis de ambiente do processo atual. O sistema operacional injeta algumas delas; o resto você define.
export DATABASE_URL="postgres://localhost/mydb"
node server.js
console.log(process.env.DATABASE_URL);
Funciona. Mas não é reproduzível. O colega que clonar o repo não sabe que precisa dessa variável, o CI não sabe, e você vai esquecer quando voltar ao projeto em três semanas.
Dois detalhes que pegam muita gente:
process.env retorna sempre strings. PORT=3000 no .env vira "3000" no código — não o número 3000. Comparar com === sem converter explode silenciosamente em checagens de porta.
Variáveis indefinidas retornam undefined, não lançam erro. process.env.CHAVE_QUE_NAO_EXISTE não quebra na hora — quebra depois, num lugar aleatório da aplicação, com uma mensagem que não aponta para a causa real.
dotenv na ordem certa
O dotenv faz uma coisa: lê o arquivo .env e popula o process.env. O problema está na ordem de execução.
npm install dotenv
import express from 'express';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
Parece certo. Não é. Em ESM, import é içado — os módulos são resolvidos antes do código rodar. Quando dotenv.config() executa, express já foi inicializado sem as variáveis do .env. Se express ou qualquer middleware lê process.env durante a importação, chegou tarde.
import 'dotenv/config';
import express from 'express';
import { connectDB } from './db.js';
const app = express();
import 'dotenv/config' também é içado, mas na ordem do arquivo — e por ser o primeiro import, executa antes de qualquer outro módulo. É a forma correta em ESM com Node 18+.
Para CommonJS, o problema não existe da mesma forma porque require é síncrono e sequencial:
require('dotenv').config();
const express = require('express');
Já vi isso derrubar uma conexão de banco em staging inteira porque DATABASE_URL chegava como undefined para o pool de conexões. O servidor subia, mas a primeira query derrubava tudo. O log não apontava para o dotenv — apontava para o banco.
O que commitar — e o que nunca commitar
.env
.env.local
.env.*.local
.env.production
Tudo isso fica fora do git. Sem exceção. Sem "mas é só o de desenvolvimento".
O que você deve commitar é o .env.example:
DATABASE_URL=
JWT_SECRET=
STRIPE_SECRET_KEY=
REDIS_URL=
PORT=3000
NODE_ENV=development
Sem valores. Só as chaves. Isso serve como documentação viva — qualquer dev que clonar o projeto sabe exatamente o que precisa configurar antes de rodar npm dev.
⚠️ Armadilha comum: colocar valores de desenvolvimento no
.env.example"para facilitar o onboarding". O problema é que alguém vai usar esses valores em produção sem perceber — especialmente em projetos com muitos contribuidores. Use placeholders descritivos:DATABASE_URL=postgres://user:password@host:5432/dbname.
Validação na inicialização: fail fast ou fail tarde?
Sem validação, a aplicação sobe normalmente com DATABASE_URL=undefined. O erro aparece na primeira requisição que tenta abrir uma conexão com o banco — longe do ponto real da falha, com uma mensagem que não ajuda a diagnosticar.
A validação mínima que todo projeto deveria ter:
const required = ['DATABASE_URL', 'JWT_SECRET', 'STRIPE_SECRET_KEY'];
for (const key of required) {
if (!process.env[key]) {
console.error(`Variável ausente: ${key}`);
process.exit(1);
}
}
Processo encerra imediatamente, com mensagem clara, antes de qualquer rota ser registrada. Deploy falha no início, não no meio.
Para TypeScript ou projetos que precisam de coerção de tipos, o zod resolve isso de forma elegante:
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, 'JWT_SECRET precisa ter ao menos 32 caracteres'),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
REDIS_URL: z.string().url().optional(),
});
export const env = envSchema.parse(process.env);
env.PORT agora é number, não string. env.JWT_SECRET com menos de 32 caracteres rejeita na inicialização com mensagem legível. E REDIS_URL é opcional — se não estiver presente, não quebra, mas se estiver presente precisa ser uma URL válida.
💡 Dica: exporte
envde um módulo central (src/config/env.ts) e importe de lá em todo o projeto. Isso eliminaprocess.env.QUALQUER_COISAespalhado pelo código e centraliza a validação em um único ponto.
Separando ambientes sem bagunça
| Arquivo | Carregado quando | Commitar? |
|---|---|---|
.env | Sempre, como base | ❌ Não |
.env.local | Sempre, sobrescreve .env | ❌ Não |
.env.development | NODE_ENV=development | ⚠️ Só sem segredos |
.env.test | NODE_ENV=test | ⚠️ Só sem segredos |
.env.production | NODE_ENV=production | ❌ Nunca |
.env.example | Nunca (é documentação) | ✅ Sim |
Para carregar o arquivo correto por ambiente:
import dotenv from 'dotenv';
dotenv.config({
path: `.env.${process.env.NODE_ENV || 'development'}`,
override: false,
});
dotenv.config();
O override: false impede que o dotenv sobrescreva variáveis que já existem no process.env quando a aplicação inicia — o que inclui variáveis injetadas pelo CI/CD ou pelo orquestrador de containers. Sem ele, um .env.development esquecido no servidor poderia sobrescrever a DATABASE_URL de produção injetada pelo Kubernetes.
Segredos em produção: comparando as alternativas reais
Usar .env em servidor de produção funciona, mas cria problemas que escalam mal: o arquivo precisa estar no servidor, precisa ser sincronizado entre instâncias, não tem controle de acesso granular e não tem histórico de auditoria.
As alternativas, em ordem de complexidade:
Variáveis da plataforma de deploy — Railway, Render, Fly.io, Vercel, Heroku: todas têm interface para definir env vars que são injetadas automaticamente no processo. Zero arquivo, zero risco de commit. Certo para a maioria dos projetos.
AWS Parameter Store — armazenamento hierárquico com controle de acesso por IAM. Gratuito para parâmetros padrão. Bom para ambientes AWS que precisam de controle de acesso mas não exigem rotação automática.
AWS Secrets Manager — similar ao Parameter Store, mas com suporte nativo a rotação automática de credenciais (banco de dados, chaves de API). Tem custo por segredo. Indicado quando compliance ou auditoria exigem rotação periódica.
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getSecret(secretName) {
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return JSON.parse(response.SecretString);
}
const { DATABASE_URL, JWT_SECRET } = await getSecret('myapp/production');
HashiCorp Vault — para infraestrutura própria que precisa de rotação automática, políticas de acesso complexas e auditoria detalhada. Overhead operacional alto. Faz sentido a partir de times com dedicação para manter a infraestrutura.
A decisão prática: se você está no Railway ou Vercel, use as env vars da plataforma. Se está na AWS com múltiplos serviços, Parameter Store ou Secrets Manager. Se tem uma infra própria grande, avalie o Vault. Não há resposta universal — há a resposta certa para o seu contexto.
Se o .env já foi commitado: o que fazer agora
Isso merece uma seção própria porque é o cenário de emergência que o artigo médio sobre dotenv ignora.
Passo 1: rotacione as credenciais imediatamente. Antes de qualquer outra coisa. Assuma que foram comprometidas. Gere novas chaves de API, troque senhas de banco, revogue tokens JWT. O histórico do git é público para qualquer pessoa com acesso ao repositório — e se o repo ficou público por um segundo que seja, considere comprometido.
Passo 2: remova do histórico com git filter-repo.
pip install git-filter-repo
git filter-repo --path .env --invert-paths
git filter-repo reescreve o histórico inteiro removendo o arquivo. É o substituto oficial do git filter-branch, que foi descontinuado por ser lento e propenso a erros.
Passo 3: force-push em todas as branches.
git push origin --force --all
git push origin --force --tags
Passo 4: notifique todos os colaboradores para que reclonem o repositório. O histórico reescrito é incompatível com o histórico local deles.
⚠️ Atenção: remover do histórico não apaga de forks, mirrors, ou de qualquer serviço que já tenha indexado o repositório (como o GitHub Secret Scanning, que já te enviou o alerta). A rotação das credenciais no passo 1 é inegociável.
FAQ
Posso usar dotenv em produção?
Não é recomendado. O dotenv é uma ferramenta de desenvolvimento — ele existe para simular o comportamento de injeção de variáveis que plataformas de produção fazem nativamente. Em produção, as variáveis devem ser injetadas pelo orquestrador, pela plataforma de deploy ou por um secret manager. Se você está rodando dotenv.config() em produção, o processo de deploy precisa ser revisado.
Qual a diferença prática entre .env e .env.local?
A convenção é: .env contém defaults sem segredos (pode eventualmente ser commitado com valores de documentação), enquanto .env.local contém os valores reais da máquina do dev e nunca é commitado. Na prática, muitos projetos usam apenas .env sem commitar nada — funciona, desde que o .env.example exista e esteja atualizado.
Como evitar que segredos apareçam em logs?
Nunca use console.log(process.env) em código que vai para produção. Para debugar variáveis específicas, logue só as chaves: console.log(Object.keys(process.env)). Se você usa um logger como Winston ou Pino, configure um redact para mascarar campos sensíveis automaticamente.
O process.env fica visível para outros processos no servidor?
Variáveis de ambiente são isoladas por processo — outros processos no mesmo servidor não enxergam o process.env da sua aplicação. O risco real é diferente: acesso ao processo em si (via /proc no Linux), logs que expõem as variáveis, ou qualquer código na aplicação que retorne process.env numa rota de API.
dotenv-safe ainda vale a pena com zod disponível?
Para projetos simples em JavaScript puro sem TypeScript, o dotenv-safe ainda é uma opção válida e mais leve — ele valida presença das variáveis contra o .env.example sem precisar de schema. Para projetos TypeScript, o zod entrega tipagem, coerção e mensagens de erro mais ricas. São ferramentas diferentes para contextos diferentes.
Próximos passos
Cinco ações para aplicar hoje, em ordem de prioridade:
- Verifique o histórico agora:
git log --all --full-history -- .env— se retornar algum commit, rotacione as credenciais antes de continuar lendo - Adicione ao
.gitignoreas linhas.env,.env.locale.env.*.local— se ainda não estiver lá - Crie o
.env.examplecom todas as chaves que sua aplicação usa, sem valores reais - Adicione validação na inicialização — o bloco mínimo com
process.exit(1)já resolve; o zod resolve melhor - Mova segredos de produção para a plataforma de deploy — Railway, Render, Fly.io ou equivalente
O próximo nível depois disso: rotação automática de credenciais com AWS Secrets Manager ou Vault, e auditoria de acesso a segredos por serviço. Mas para a maioria dos projetos, os cinco passos acima já eliminam 95% do risco real.