Variáveis de ambiente em Node.js: .env e segurança

Variáveis de ambiente são o mecanismo padrão para separar configuração de código, conexão ao banco, chaves de API e segredos nunca devem estar no código-fonte. Em Node.js, o dotenv carrega o arquivo .env em desenvolvimento; em produção na VPS, as variáveis vêm do docker-compose.yml ou do sistema operacional.

dotenv: carregando .env em desenvolvimento

dotenv é o pacote padrão para carregar variáveis de ambiente de um arquivo .env:

typescript
# Instalar dotenv
npm install dotenv

# Carregar o mais cedo possível (primeiro import da aplicação)
# src/index.ts ou src/index.js
import 'dotenv/config'    // ESM
// ou
require('dotenv').config() // CJS

// .env (nunca commitado no git)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=minha_chave_secreta_muito_longa_e_aleatória
PORT=3000
NODE_ENV=development

# .env.example (commitado — documenta as variáveis necessárias)
DATABASE_URL=
JWT_SECRET=
PORT=3000
NODE_ENV=development

Validar variáveis de ambiente com Zod

Falhar no startup por variável ausente é melhor que falhar em runtime com erro críptico. Zod valida e tipifica todas as variáveis na inicialização:

typescript
// src/env.ts
import { z } from 'zod'

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET deve ter ao menos 32 caracteres'),
  REDIS_URL: z.string().url().optional(),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
})

// Validar imediatamente — lança erro no startup se inválido
export const env = envSchema.parse(process.env)

// Uso:
import { env } from './env'
console.log(env.PORT)         // number (tipado)
console.log(env.DATABASE_URL) // string (garantida válida)

Variáveis de ambiente em produção (Docker)

Em produção com Docker, as variáveis vêm do docker-compose.yml ou de um arquivo .env do Compose, nunca de um .env dentro da imagem:

yaml
# docker-compose.yml
services:
  app:
    image: minha-api:latest
    env_file:
      - .env.production    # arquivo .env no host (não na imagem)
    environment:
      NODE_ENV: production  # override de variável específica

# .env.production (no host da VPS, não commitado)
DATABASE_URL=postgresql://user:pass@postgres:5432/db
JWT_SECRET=chave_longa_gerada_com_openssl_rand_hex_32
PORT=3000

# Verificar variáveis resolvidas antes de subir
docker compose config | grep -A5 "environment:"

# Injetar variável sem arquivo (útil em CI/CD)
JWT_SECRET=abc123 docker compose up -d

Gerar segredos seguros

Chaves JWT, session secrets e API keys devem ser geradas de forma criptograficamente segura:

bash
# Gerar chave aleatória de 64 caracteres hex (128 bits)
openssl rand -hex 32

# Para JWT secret (256 bits)
openssl rand -base64 64

# No Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# Para UUID como secret (não ideal mas funcional)
node -e "console.log(require('crypto').randomUUID())"

# Exemplo de .env com chaves geradas:
JWT_SECRET=a3f8b2c9d4e7f1a0b3c6d9e2f5a8b1c4d7e0f3a6b9c2d5e8f1a4b7c0d3e6f9a2
SESSION_SECRET=K+5J8mN2pQ7rT0vX3yB6eH9kL1nP4sV7wY0zC3fG6hJ9mO2qR5uW8xA1dF4iI7lN

Diferentes ambientes: dev, staging, produção

Organize variáveis por ambiente com arquivos separados:

typescript
# Estrutura de arquivos de ambiente
.env                  # desenvolvimento local (não commitado)
.env.example          # template (commitado — sem valores)
.env.test             # testes automatizados (pode ter valores fake)

# .gitignore — garantir que .env nunca seja commitado
.env
.env.production
.env.staging
*.local

# Carregar ambiente específico com dotenv
import dotenv from 'dotenv'

const envFile = process.env.ENV_FILE || '.env'
dotenv.config({ path: envFile })

# Iniciar com env específico:
ENV_FILE=.env.staging node dist/index.js

# Com docker-compose.override.yml para desenvolvimento
# (sobrescreve configurações de docker-compose.yml)
# docker-compose.override.yml:
# services:
#   app:
#     volumes:
#       - .:/app
#     command: npm run dev

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

process.env é Node.js (server-side). import.meta.env é Vite/build tools (client-side). Para backend Node.js: sempre process.env + dotenv. Para frontend com Vite: import.meta.env.VITE_MINHA_VAR. NUNCA coloque segredos em variáveis do frontend (VITE_), elas ficam visíveis no bundle JavaScript do browser.

Aceite múltiplos secrets durante a transição: gere o novo secret, configure a API para verificar tokens com o secret antigo E o novo (try/catch), faça deploy, aguarde todos os tokens antigos expirarem (ex: 24h), remova o secret antigo. Forçar logout de todos: altere apenas o secret antigo e todos os tokens existentes ficam inválidos imediatamente.

Nunca faça console.log(process.env) em produção. Para debug seguro: console.log(Object.keys(process.env).filter(k => !k.includes("SECRET") && !k.includes("PASSWORD") && !k.includes("KEY"))). Considere uma função sanitize que mascara valores sensíveis antes de logar.

Docker Secrets (modo Swarm) monta o secret como arquivo em /run/secrets/, não aparece em docker inspect nem em variáveis de ambiente. Para Compose sem Swarm, variáveis de ambiente são suficientes desde que: o .env não esteja na imagem, não apareça em logs, e o host tenha acesso restrito. Para máxima segurança: HashiCorp Vault ou AWS Secrets Manager.

GitHub Actions Secrets (Settings → Secrets): defina as variáveis no repositório. No workflow: echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> /tmp/.env && scp /tmp/.env user@vps:/opt/app/.env.production. Alternativa mais segura: gere um .env na VPS via script Ansible ou SSH com as variáveis injetadas pelo CI sem salvar em arquivo intermediário.