Variáveis de ambiente no Node: process.env e o arquivo .env
Como ler process.env, carregar o .env com o --env-file nativo do Node, separar dev de produção e não deixar segredo vazar no repositório.
Variável de ambiente é um valor que vem de fora do código. O Node coloca todas
elas em process.env quando o programa sobe, e é assim que a mesma API aponta
para o banco da sua máquina hoje e para o banco de produção amanhã, sem trocar
uma linha.
Os exemplos são todos da agenda da Pata Amiga, uma clínica veterinária. É uma API em Express que marca consultas, guarda tudo no Postgres e manda SMS de lembrete para os tutores. Porta, endereço do banco e chave do provedor de SMS são exatamente as três coisas que mudam de máquina para máquina — e por isso nenhuma delas pode estar escrita no código.
O mesmo aparelho, tomadas de lugares diferentes
Uma cafeteira continua sendo a mesma quando sai da sua casa e vai para um escritório, mas endereço, voltagem e tomada ao redor podem mudar. Uma aplicação também deve manter o mesmo código enquanto recebe de cada ambiente a porta, a URL do banco e as credenciais adequadas. Esses valores externos são as variáveis de ambiente.
No Node, process.env é a caixa de configuração entregue ao processo quando
ele começa. Ela não é um cofre mágico: os valores existem na memória do
programa, chegam sempre como strings e ainda precisam ser validados. Antes de
criar .env, liste o que realmente muda entre sua máquina e produção. Se algo
é regra fixa da aplicação, fica no código; se depende do lugar ou é segredo,
entra pela configuração. Essa separação evita usar .env como gaveta para
qualquer constante.
process.env: de onde essas variáveis realmente vêm
process.env é um objeto que o Node monta uma única vez, no boot, a partir do
ambiente que o sistema operacional entregou ao processo. Quem entrega é quem
chamou o node — normalmente o seu terminal.
console.log('porta:', process.env.PORT);
console.log('banco:', process.env.DATABASE_URL);
console.log('clinica:', process.env.NOME_CLINICA);Rodando esse arquivo direto, sem nenhum preparo:
node config.jsNada quebrou. Variável que não existe vira undefined, e é isso que torna o
problema traiçoeiro: a API sobe achando que está tudo certo e só falha na hora
de conectar no banco.
Agora com um valor na frente do comando:
PORT=4477 node config.jsEsse PORT=4477 antes do node não é sintaxe do Node: é o shell criando uma
variável válida só para aquele processo filho. Feche o terminal e ela some.
Boa parte do que está em process.env você nunca escreveu:
console.log('SHELL:', process.env.SHELL);
console.log('LANG:', process.env.LANG);
console.log('total herdado:', Object.keys(process.env).length);
console.log('PORT:', process.env.PORT);Cinquenta e nove variáveis no meu terminal, herdadas do zsh sem ninguém pedir. O número muda de máquina para máquina — o que não muda é o mecanismo: o processo filho recebe uma cópia do ambiente do pai.
E tem um detalhe que vai importar mais adiante: o Node converte tudo para texto na hora da atribuição.
process.env.CONSULTAS_POR_DIA = 30;
console.log(typeof process.env.CONSULTAS_POR_DIA, process.env.CONSULTAS_POR_DIA);Guardei um número e li uma string. Isso não é acidente do exemplo: é a regra do objeto inteiro.
–env-file: o Node 24 lê o .env sem instalar dotenv
Digitar cinco variáveis na frente de todo comando é inviável. A solução de
sempre foi um arquivo .env na raiz do projeto — e desde o Node 20.6 ele é
lido pelo próprio Node, sem dependência nenhuma.
Este é o .env da agenda:
PORT=4477
NOME_CLINICA=Pata Amiga
DATABASE_URL=postgres://vet:senha_local@localhost:5432/agenda
SMS_API_KEY=sk_teste_9f3c1b7a
LEMBRETE_SMS=false
CONSULTAS_POR_DIA=24E é só apontar a flag para ele:
node --env-file=.env config.jsRepare que o arquivo não é lido pelo seu código: quem lê é o Node, antes de a
primeira linha do programa executar. Por isso config.js continuou idêntico.
Agora a parte que quase ninguém testa. O que ganha quando a variável está nos dois lugares?
PORT=9999 node --env-file=.env config.jsO ambiente real venceu o arquivo. O .env só preenche o que ainda não existe.
Essa precedência é o que faz o mesmo projeto funcionar na sua máquina e no
servidor: lá o .env nem existe, e a plataforma de deploy injeta a PORT dela
direto no ambiente.
Se o arquivo apontado não existir, o Node se recusa a subir:
node --env-file=.env.producao config.jsO processo termina com código de saída 9, o que derruba o npm start junto —
bom comportamento, porque subir sem configuração é pior do que não subir. Para
o caso em que o arquivo é opcional, existe a variante tolerante:
node --env-file-if-exists=.env.producao config.jsNa prática a flag vive no package.json, para ninguém precisar lembrar dela:
{
"name": "agenda-pata-amiga",
"private": true,
"type": "module",
"scripts": {
"dev": "node --watch --env-file=.env servidor.js",
"start": "node servidor.js"
}
}O dev carrega o .env e reinicia sozinho a cada salvamento. O start, que é
o comando de produção, não carrega arquivo nenhum: lá as variáveis vêm do
ambiente de verdade.
Quando o dotenv ainda faz sentido no projeto
O dotenv é o pacote que resolvia isso antes de o Node resolver. Ele continua
existindo e funciona assim:
import 'dotenv/config';
console.log('porta:', process.env.PORT);A pergunta honesta é: o parser nativo é tão bom quanto o dele? Testei os casos
que costumam quebrar parser caseiro — comentário, aspas com cerquilha dentro,
prefixo export e valor de várias linhas:
# credenciais do provedor de SMS
SMS_REMETENTE="Pata Amiga # 24h"
export SMS_API_KEY=sk_teste_9f3c1b7a
CHAVE_ASSINATURA="-----INICIO-----
linha-do-meio
-----FIM-----"console.log('REMETENTE:', JSON.stringify(process.env.SMS_REMETENTE));
console.log('API_KEY :', JSON.stringify(process.env.SMS_API_KEY));
console.log('ASSINATURA:', JSON.stringify(process.env.CHAVE_ASSINATURA));Primeiro pelo Node, depois pelo dotenv 17.4.2:
node --env-file=.env.dificil dificil.js
DOTENV_CONFIG_PATH=.env.dificil node -r dotenv/config dificil.jsByte por byte igual. O # dentro das aspas não virou comentário, o export
foi ignorado como prefixo e as três linhas da chave viraram um valor só, com
\n no meio.
E onde os dois falham junto? Em interpolação:
DB_HOST=localhost
DB_USUARIO=vet
DATABASE_URL=postgres://${DB_USUARIO}@${DB_HOST}:5432/agendanode --env-file=.env.expand -p "process.env.DATABASE_URL"Nenhum dos dois expande ${} — isso é coisa de shell, e no .env você
recebe o texto cru. Quem precisa disso instala o dotenv-expand; quem não
precisa escreve a URL inteira e segue a vida.
| situação | use | por quê |
|---|---|---|
API que sobe por npm run dev |
--env-file |
zero dependência, e o arquivo é lido antes do seu código |
| projeto que ainda roda em Node 18 | dotenv |
a flag nativa não existe nessa versão |
carregar .env no meio do código, condicionalmente |
dotenv |
é função, dá para chamar quando você quiser |
precisa de ${VAR} dentro do .env |
dotenv + dotenv-expand |
nenhum parser faz isso sozinho |
| produção | nenhum dos dois | as variáveis já vêm do ambiente da plataforma |
Tudo em process.env é string, inclusive a palavra “false”
Este é o bug que aparece em toda API que tem uma chave liga-desliga. A clínica
tem uma: LEMBRETE_SMS=false, porque em desenvolvimento ninguém quer disparar
SMS de verdade.
console.log('valor:', process.env.LEMBRETE_SMS);
console.log('tipo:', typeof process.env.LEMBRETE_SMS);
if (process.env.LEMBRETE_SMS) {
console.log('enviando SMS de lembrete para os tutores...');
} else {
console.log('lembrete por SMS desligado');
}A variável vale false e o if entrou mesmo assim. O motivo é simples quando
você olha a segunda linha: o valor é a string 'false', e qualquer string
com pelo menos um caractere é
um valor truthy. 'false', '0' e 'nao'
entram todos no if.
O mesmo vale para número:
node --env-file=.env -p "process.env.PORT + 1"A conta deu 44771 porque + entre string e número concatena em vez de
somar: o Node juntou '4477' com '1'. Em vez de
lembrar disso em cada uso, converta tudo uma vez só, num módulo de
configuração:
const bool = (valor) => valor === 'true';
export const config = {
porta: Number(process.env.PORT),
clinica: process.env.NOME_CLINICA,
lembreteSms: bool(process.env.LEMBRETE_SMS),
consultasPorDia: Number(process.env.CONSULTAS_POR_DIA),
};
console.log(config);
console.log('porta + 1 =', config.porta + 1);
console.log('vagas restantes =', config.consultasPorDia - 18);Agora porta é número, lembreteSms é booleano de verdade e o resto do
projeto importa config em vez de tocar em process.env. Repare no bool:
ele trata como ligado só a palavra exata true. Qualquer outra coisa —
false, 0, vazio, um erro de digitação — desliga. Num interruptor que dispara
SMS para clientes, errar para o lado desligado é a escolha certa.
Validando as variáveis no boot, antes de a API subir
Se a SMS_API_KEY estiver faltando, o melhor momento para descobrir isso é o
primeiro segundo da aplicação — não a primeira consulta marcada às três da
manhã. O módulo de configuração vira, então, o portão de entrada:
const obrigatorias = ['PORT', 'DATABASE_URL', 'SMS_API_KEY'];
const faltando = obrigatorias.filter((nome) => !process.env[nome]);
if (faltando.length > 0) {
console.error(`[config] a API não subiu: falta ${faltando.join(', ')}`);
console.error('[config] copie o .env.example para .env e preencha os valores');
process.exit(1);
}
export const config = {
porta: Number(process.env.PORT),
clinica: process.env.NOME_CLINICA ?? 'Clínica sem nome',
bancoUrl: process.env.DATABASE_URL,
smsApiKey: process.env.SMS_API_KEY,
lembreteSms: process.env.LEMBRETE_SMS === 'true',
consultasPorDia: Number(process.env.CONSULTAS_POR_DIA ?? 20),
};O !process.env[nome] pega os dois casos que interessam: a variável ausente
(undefined) e a variável declarada e vazia (SMS_API_KEY=), que é o erro
mais comum de quem copiou o .env.example e esqueceu de preencher.
O servidor só importa a configuração pronta:
import express from 'express';
import { config } from './env.js';
const app = express();
app.get('/saude', (req, res) => {
res.json({
clinica: config.clinica,
consultasPorDia: config.consultasPorDia,
lembreteSms: config.lembreteSms,
});
});
app.listen(config.porta, () => {
console.log(`[api] ${config.clinica} ouvindo na porta ${config.porta}`);
});Esquecendo o --env-file, o resultado é uma mensagem que diz o que fazer, e não
um stack trace de driver de banco:
node --run startCom um .env pela metade, a mensagem fica ainda mais precisa — ela nomeia
exatamente a variável que ficou para trás:
node --env-file=.env.parcial servidor.jsE com tudo no lugar:
node --env-file=.env servidor.js &
curl -s http://localhost:4477/saudeOito linhas de validação, e o erro de configuração passou a se explicar
sozinho. Quando a lista de variáveis cresce e você quer validar formato — que
DATABASE_URL seja uma URL, que PORT seja um número entre 1 e 65535 —, vale
trocar o filter por um schema, com a mesma biblioteca que
valida a entrada da API.
.env, .env.example e a linha do .gitignore
O .env fica na sua máquina e nunca entra no Git. Mas quem clona o projeto
precisa saber o que preencher — e é esse o trabalho do .env.example, que vai
versionado, com as mesmas chaves, valores fictícios ou em branco, e um
comentário em cada uma:
# Porta em que a API sobe. Em produção quem define é a plataforma.
PORT=4477
# Nome que aparece no rodapé do e-mail e no /saude
NOME_CLINICA=Pata Amiga
# Conexão com o Postgres da agenda
DATABASE_URL=postgres://usuario:senha@localhost:5432/agenda
# Chave do provedor de SMS — peça a sua no painel
SMS_API_KEY=
# Servidor de e-mail para a confirmação de consulta
SMTP_HOST=Os dois arquivos desandam com o tempo: alguém adiciona uma variável no .env
e esquece de documentar no exemplo. Vinte linhas resolvem, usando o mesmo
node:fs da lição sobre
ler e escrever arquivo:
import { readFileSync } from 'node:fs';
const chaves = (arquivo) =>
readFileSync(arquivo, 'utf8')
.split('\n')
.map((linha) => linha.trim())
.filter((linha) => linha && !linha.startsWith('#'))
.map((linha) => linha.split('=')[0]);
const doExemplo = chaves('.env.example');
const doLocal = chaves('.env');
console.log('faltam no seu .env :', doExemplo.filter((c) => !doLocal.includes(c)).join(', ') || 'nada');
console.log('não documentadas :', doLocal.filter((c) => !doExemplo.includes(c)).join(', ') || 'nada');Ele achou as duas divergências reais do meu projeto. Rodando isso no dev
antes do servidor, a desatualização vira um aviso na sua tela em vez de um
“aqui não funciona” no chat do time.
Falta a linha mais importante do projeto inteiro:
.env
.env.localEssas duas linhas vão no .gitignore antes do primeiro commit. A próxima
seção mostra o que acontece com quem coloca depois.
NODE_ENV: o que ele muda de verdade e o que é lenda
Começa com a parte que surpreende: o Node não define NODE_ENV e não olha para
ela. Para o runtime, é uma variável comum, sem significado especial. Quem dá
sentido a ela são as bibliotecas — o Express, entre elas.
import express from 'express';
const app = express();
const porta = Number(process.env.PORT);
app.get('/consultas', () => { throw new Error('banco de dados indisponível'); });
console.log('NODE_ENV =', process.env.NODE_ENV, '| app.get(env) =', app.get('env'));
app.listen(porta, () => console.log('ouvindo em', porta));Subindo o mesmo arquivo duas vezes, uma com a variável e outra sem:
PORT=4481 node amb2.js &
PORT=4482 NODE_ENV=production node amb2.js &Sem a variável, o Express assume development sozinho. Agora a rota que
estoura de propósito, nas duas portas:
curl -s http://localhost:4481/consultas | wc -c
curl -s http://localhost:4482/consultas | wc -cOitocentos e cinquenta e sete bytes de diferença, e eles são o stack trace:
curl -s http://localhost:4481/consultas | tr '>' '>\n' | sed -n '/<pre/,$p' | head -c 200Com NODE_ENV=production, a mesma requisição devolve só isto:
curl -s http://localhost:4482/consultas | sed -n '8p'Caminho de arquivo, nome de dependência e linha de código — tudo isso ia para qualquer pessoa que soubesse quebrar sua rota, só porque a variável estava vazia. Essa é a mudança concreta, e ela vale a pena.
O que é lenda: NODE_ENV=production não deixa o Node mais rápido, não muda
nada em fs, http ou no event loop, e não é um interruptor de “modo
produção” do runtime. Fora do Express, quem também olha para ela é o npm
(npm ci --omit=dev não instala dependências de desenvolvimento) e os
empacotadores de front-end, que usam o valor para escolher o build.
Segredo commitado por engano: o que fazer agora
Acontece com todo mundo uma vez. O git add . no primeiro dia do projeto, o
.gitignore que ainda não existia, e o .env de produção dentro do commit:
git add .
git commit -m "primeira versao da API da agenda"
git ls-filesO reflexo é criar o .gitignore na hora. Só que ele não desfaz nada — ele só
vale para arquivos que o Git ainda não rastreia:
echo ".env" > .gitignore
git add .gitignore && git commit -m "adiciona .gitignore"
git status --shortO .env continua rastreado, e o Git continua vendo as alterações dele. Para
tirar do rastreamento sem apagar o arquivo da sua máquina:
git rm --cached .env
git commit -m "tira o .env do versionamento"
git ls-filesParece resolvido. Não está:
git log --oneline
git show ca94c60:.envA senha do banco e a chave do provedor de SMS continuam ali, legíveis, em um commit antigo — e em cada clone que alguém já fez do repositório. Reescrever o histórico ajuda, mas não alcança as cópias que já saíram.
Por isso a ordem de prioridade é esta, e nesta ordem:
- Revogue a chave. Gere uma nova no painel do provedor e troque a senha do banco. Enquanto o segredo antigo funcionar, ele é um segredo vazado.
- Coloque
.envno.gitignoree rodegit rm --cached .env. - Só então cuide do histórico, se o repositório for privado e pequeno.
- Adicione o
.env.exampleno lugar, para o próximo a clonar não repetir.
O passo 1 é o único que resolve de fato. Os outros três impedem a próxima vez.
O que vem depois
Com a configuração fora do código, a API da clínica está pronta para sair da sua
máquina. O próximo passo natural é entender
o servidor HTTP por baixo do Express, montar
a primeira rota de verdade e, quando estiver
de pé, colocar a API no ar — onde a PORT deixa de
vir do .env e passa a vir da plataforma. O mapa completo está no
guia de Node.js e na
trilha de Node.
Perguntas frequentes
Preciso instalar o dotenv no Node 24?
O .env deve ir para o repositório?
Como passo variável de ambiente no deploy, sem arquivo .env?
Posso mudar uma variável com o servidor já rodando?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- Node.js — CLI: --env-file — nodejs.org
- Node.js — process.env — nodejs.org
- The Twelve-Factor App — Config — 12factor.net


