Cada aplicación tiene configuración que cambia según dónde se ejecuta: en tu ordenador de desarrollo usas una base de datos local, en producción conectas a un servidor gestionado; en desarrollo las claves de API son de prueba, en producción son reales. Las variables de entorno son el mecanismo estándar para separar esa configuración del código, y son también la forma correcta de manejar secretos como contraseñas, tokens y claves de API sin exponerlos en el repositorio.
En esta guía aprenderás qué son, cómo usarlas en Python y Node.js, cómo organizarlas con archivos .env y cómo evitar los errores de seguridad más habituales.
Qué es una variable de entorno
Una variable de entorno es un par clave-valor que existe en el entorno del sistema operativo donde se ejecuta un proceso. Cada proceso hereda las variables de entorno de su proceso padre y puede leer su valor en cualquier momento durante la ejecución.
# Ver las variables de entorno actuales en la terminal
env # Linux/macOS: muestra todas
printenv # también Linux/macOS
printenv PATH # ver el valor de una variable específica
echo $PATH # forma abreviada en bash/zsh
# En Windows (PowerShell)
Get-ChildItem Env:
$Env:PATH
# Definir una variable de entorno en la terminal (solo para esa sesión)
export MI_VARIABLE="hola" # bash/zsh (Linux/macOS)
echo $MI_VARIABLE # hola
# Disponible para el proceso actual y sus hijos
python3 -c "import os; print(os.getenv('MI_VARIABLE'))" # hola
# Al cerrar la terminal, la variable desaparece
# Para que persista, añadirla a ~/.bashrc o ~/.zshrc
# Definir una variable solo para un proceso específico (sin export)
MI_VARIABLE=hola python3 mi_script.py
# MI_VARIABLE existe durante la ejecución de mi_script.py pero no en la terminal
Por qué no hardcodear la configuración en el código
# ❌ Configuración hardcodeada: el error más habitual
# Python
import psycopg2
conn = psycopg2.connect(
host="prod-db.empresa.com",
user="admin",
password="contraseña_super_secreta", # ← expuesta en el código
database="produccion"
)
API_KEY = "sk-1234567890abcdef" # ← en el repositorio de Git
# JavaScript/Node.js
const stripe = require('stripe')('sk_live_abcdef123456'); # ← clave real hardcodeada
const DB_URL = 'postgresql://admin:secreto@prod.db.com/app';
Cuando haces un commit con credenciales en el código, esas credenciales quedan en el historial de Git para siempre, incluso si las borras en el siguiente commit. Si el repositorio es público o alguien obtiene acceso a él, esas credenciales están comprometidas. Esto ocurre con más frecuencia de lo que parece y ha causado incidentes de seguridad graves en empresas grandes.
Además de la seguridad, hay un problema práctico: no puedes reutilizar el mismo código en distintos entornos (desarrollo, staging, producción) si la configuración está hardcodeada.
El archivo .env: configuración local de desarrollo
El archivo .env es un archivo de texto en la raíz del proyecto que contiene las variables de entorno para el entorno local de desarrollo. La aplicación lo lee al arrancar y carga las variables.
# .env — configuración local de desarrollo
# Este archivo NUNCA debe subirse al repositorio
# Base de datos
DATABASE_URL=postgresql://usuario:contraseña@localhost:5432/mi_app_dev
DATABASE_POOL_SIZE=5
# Autenticación
JWT_SECRET=desarrollo-secreto-no-usar-en-produccion-abc123
JWT_EXPIRA_HORAS=24
# APIs externas (claves de prueba/sandbox)
STRIPE_SECRET_KEY=sk_test_abcdef1234567890
STRIPE_WEBHOOK_SECRET=whsec_test_abc123
# Email (servicio de prueba que no envía emails reales)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USER=
SMTP_PASSWORD=
EMAIL_FROM=noreply@localhost
# Configuración general
ENTORNO=desarrollo
DEBUG=true
NIVEL_LOG=debug
PUERTO=3000
URL_BASE=http://localhost:3000
# .gitignore — asegurarse de que .env NUNCA se suba a Git
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# No ignorar el archivo de ejemplo (sin secretos reales)
# !.env.example ← se puede incluir esta línea para que .env.example SÍ se suba
# .env.example — plantilla sin valores reales (SÍ se sube al repositorio)
# Copia este archivo como .env y rellena los valores
DATABASE_URL=postgresql://usuario:contraseña@localhost:5432/nombre_base_datos
JWT_SECRET=genera-una-clave-aleatoria-larga
STRIPE_SECRET_KEY=sk_test_... # obtener en dashboard.stripe.com
SMTP_HOST=
SMTP_PORT=587
EMAIL_FROM=noreply@tudominio.com
ENTORNO=desarrollo
PUERTO=3000
Leer variables de entorno en Python
# Python: módulo os de la librería estándar
import os
# os.environ: diccionario con todas las variables de entorno
print(os.environ)
# Leer una variable (lanza KeyError si no existe)
db_url = os.environ["DATABASE_URL"]
# Leer con valor por defecto (si no existe, devuelve el default)
debug = os.environ.get("DEBUG", "false")
puerto = os.environ.get("PUERTO", "3000")
# Convertir al tipo correcto (las variables de entorno son siempre strings)
debug_bool = os.environ.get("DEBUG", "false").lower() == "true"
puerto_int = int(os.environ.get("PUERTO", "3000"))
pool_size = int(os.environ.get("DATABASE_POOL_SIZE", "10"))
# python-dotenv: cargar el archivo .env automáticamente
# pip install python-dotenv
from dotenv import load_dotenv
import os
# Cargar .env al inicio de la aplicación
load_dotenv() # busca .env en el directorio actual
# A partir de aquí, os.environ tiene las variables del .env
db_url = os.environ["DATABASE_URL"]
jwt_secret = os.environ["JWT_SECRET"]
# load_dotenv no sobreescribe variables ya definidas en el entorno del sistema
# Las variables del sistema tienen prioridad sobre las del .env
# Esto es importante: en producción las defines en el servidor, no en .env
# Patrón recomendado: módulo de configuración centralizado
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
def requerir(clave: str) -> str:
"""Leer una variable obligatoria. Falla al arrancar si no está definida."""
valor = os.environ.get(clave)
if valor is None:
raise ValueError(
f"Variable de entorno requerida no encontrada: {clave}\n"
f"Asegúrate de que está definida en el archivo .env o en el entorno."
)
return valor
class Config:
# Base de datos
DATABASE_URL: str = requerir("DATABASE_URL")
DATABASE_POOL: int = int(os.environ.get("DATABASE_POOL_SIZE", "10"))
# Autenticación
JWT_SECRET: str = requerir("JWT_SECRET")
JWT_EXPIRA_HORAS: int = int(os.environ.get("JWT_EXPIRA_HORAS", "24"))
# Stripe
STRIPE_SECRET: str = requerir("STRIPE_SECRET_KEY")
# Configuración general
ENTORNO: str = os.environ.get("ENTORNO", "desarrollo")
DEBUG: bool = os.environ.get("DEBUG", "false").lower() == "true"
PUERTO: int = int(os.environ.get("PUERTO", "3000"))
@property
def es_produccion(self) -> bool:
return self.ENTORNO == "produccion"
config = Config()
# Uso en el resto de la aplicación
from config import config
print(config.DATABASE_URL)
print(config.es_produccion)
# Con Pydantic (FastAPI): validación y tipos automáticos
# pip install pydantic-settings
from pydantic_settings import BaseSettings
from pydantic import PostgresDsn, AnyHttpUrl
class Settings(BaseSettings):
# Pydantic lee automáticamente las variables de entorno
# y valida los tipos
database_url: PostgresDsn # valida que es una URL de PostgreSQL válida
jwt_secret: str
jwt_expira_horas: int = 24
stripe_secret_key: str
entorno: str = "desarrollo"
debug: bool = False
puerto: int = 3000
url_base: AnyHttpUrl = "http://localhost:3000"
class Config:
env_file = ".env" # leer del archivo .env
case_sensitive = False # DATABASE_URL y database_url son la misma
settings = Settings()
# Si falta una variable obligatoria, Pydantic lanza un error detallado al arrancar:
# pydantic.error_wrappers.ValidationError: 1 validation error for Settings
# jwt_secret
# field required (type=value_error.missing)
Leer variables de entorno en Node.js
# Node.js: process.env contiene todas las variables de entorno
const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;
const puerto = parseInt(process.env.PORT || '3000', 10);
const debug = process.env.DEBUG === 'true';
// Si una variable obligatoria no está definida, su valor es undefined
// Es buena práctica verificarlo al arrancar
if (!process.env.JWT_SECRET) {
console.error('ERROR: JWT_SECRET no está definido');
process.exit(1); // salir con error: no arrancar sin configuración correcta
}
# dotenv: cargar .env en Node.js
# npm install dotenv
// Cargarlo lo antes posible, antes de cualquier otro require/import
require('dotenv').config(); // CommonJS
// O en ESModules:
import 'dotenv/config';
// A partir de aquí, process.env tiene las variables del .env
const app = require('./app');
# Desde Node.js 20.6, puedes cargar .env sin ninguna librería:
node --env-file=.env mi-script.js
# O con la variable de entorno NODE_OPTIONS:
NODE_OPTIONS='--env-file=.env' node mi-script.js
# Módulo de configuración centralizado en Node.js
// config.js
require('dotenv').config();
function requerir(clave) {
const valor = process.env[clave];
if (valor === undefined || valor === '') {
throw new Error(
`Variable de entorno requerida no encontrada: ${clave}\n` +
`Asegúrate de que está definida en .env o en el entorno del servidor.`
);
}
return valor;
}
const config = {
// Base de datos
databaseUrl: requerir('DATABASE_URL'),
databasePool: parseInt(process.env.DATABASE_POOL_SIZE || '10', 10),
// Autenticación
jwtSecret: requerir('JWT_SECRET'),
jwtExpiraHoras: parseInt(process.env.JWT_EXPIRA_HORAS || '24', 10),
// Stripe
stripeSecret: requerir('STRIPE_SECRET_KEY'),
// General
entorno: process.env.NODE_ENV || 'development',
debug: process.env.DEBUG === 'true',
puerto: parseInt(process.env.PORT || '3000', 10),
get esProduccion() {
return this.entorno === 'production';
}
};
// Validar al arrancar
Object.freeze(config); // evitar modificaciones accidentales en runtime
module.exports = config;
// Uso en el resto de la aplicación:
const config = require('./config');
app.listen(config.puerto);
Variables de entorno en distintos entornos
# Estructura recomendada de archivos de entorno
# .env → desarrollo local (en .gitignore, no se sube)
# .env.example → plantilla sin secretos (sí se sube a Git)
# .env.test → configuración para tests (puede subirse si no tiene secretos)
# .env.production → NO existe como archivo: en producción se definen en el servidor
# Convención de NODE_ENV en Node.js:
# development → entorno local
# test → tests automatizados
# production → producción
NODE_ENV=production node servidor.js
# dotenv-flow: carga automáticamente el archivo correcto según NODE_ENV
# npm install dotenv-flow
require('dotenv-flow').config();
# Carga: .env, .env.local, .env.${NODE_ENV}, .env.${NODE_ENV}.local
# (en ese orden, el último sobreescribe al anterior)
Variables de entorno en producción
En producción, el archivo .env no existe. Las variables se definen directamente en la plataforma o servidor donde se despliega la aplicación.
# Heroku
heroku config:set JWT_SECRET=valor-secreto-produccion
heroku config:set DATABASE_URL=postgresql://...
heroku config # listar todas las variables configuradas
heroku config:unset DEBUG # eliminar una variable
# Railway, Render, Fly.io
# Tienen un panel de Variables de Entorno en su dashboard web
# Docker
docker run -e JWT_SECRET=secreto -e DATABASE_URL=... mi-imagen
# Docker Compose
# docker-compose.yml:
# services:
# app:
# environment:
# - JWT_SECRET=${JWT_SECRET} # tomar del entorno del host
# - DATABASE_URL=${DATABASE_URL}
# env_file:
# - .env.produccion # cargar desde archivo
# Variables de entorno en GitHub Actions (CI/CD)
# secrets.JWT_SECRET se define en GitHub → Settings → Secrets and variables
# Se referencia en el workflow como: ${{ secrets.JWT_SECRET }}
Buenas prácticas de seguridad
# ─── 1. Generar secretos seguros ──────────────────────────────────────────────
# Python: generar un JWT_SECRET seguro
python3 -c "import secrets; print(secrets.token_hex(32))"
# c2f8a3d9b45e1f7820364a9bc58d1e72f94c06a8b3d5e1f2097a4b8c6d3e9f0
# Node.js: lo mismo
node -e "const crypto=require('crypto'); console.log(crypto.randomBytes(32).toString('hex'))"
# OpenSSL
openssl rand -hex 32
# ─── 2. Nunca loggear variables de entorno ────────────────────────────────────
# ❌ Expone todos los secretos en los logs
console.log('Configuración:', process.env);
print(os.environ)
# ✅ Solo loggear lo que es seguro mostrar
console.log('Entorno:', process.env.NODE_ENV);
console.log('Puerto:', process.env.PORT);
# Nunca loggear: JWT_SECRET, DATABASE_URL, STRIPE_SECRET_KEY, SMTP_PASSWORD...
# ─── 3. Validar al arrancar, no en runtime ────────────────────────────────────
# ❌ Descubrir que falta una variable cuando ya está en producción
app.get('/pagar', async (req, res) => {
const stripe = require('stripe')(process.env.STRIPE_SECRET); // falla aquí en prod
});
# ✅ Fallar rápido al arrancar: mejor descubrirlo en el despliegue
// En config.js, al importar el módulo:
const stripeSecret = requerir('STRIPE_SECRET_KEY'); // lanza error al arrancar si falta
# ─── 4. Principio de mínimo privilegio ───────────────────────────────────────
# La clave de base de datos para la app de lectura no necesita permisos de escritura
# La clave de Stripe para mostrar productos no necesita poder cobrar
# Crea credenciales específicas con solo los permisos necesarios
# ─── 5. Rotar secretos periódicamente ────────────────────────────────────────
# Los secretos deben tener una vida útil limitada
# Si sospechas que un secreto fue expuesto: rotarlo inmediatamente
# 1. Generar el nuevo secreto
# 2. Actualizarlo en todos los entornos
# 3. Revocar el anterior
# Procedure para rotar un JWT_SECRET sin downtime:
# 1. Añadir JWT_SECRET_NUEVO junto al JWT_SECRET_ACTUAL
# 2. Verificar tokens con ambos (primero el nuevo, luego el actual)
# 3. Emitir nuevos tokens solo con JWT_SECRET_NUEVO
# 4. Esperar a que expiren los tokens del JWT_SECRET_ACTUAL
# 5. Eliminar JWT_SECRET_ACTUAL
Qué hacer si subes un secreto a Git por error
# Si accidentalmente subes credenciales a un repositorio:
# PASO 1: Revocar el secreto INMEDIATAMENTE
# No esperes a limpiar el historial: asume que está comprometido
# - Rota la contraseña de la base de datos
# - Revoca la API key de Stripe/AWS/GitHub
# - Regenera el JWT_SECRET (invalidará todas las sesiones activas)
# PASO 2: Eliminar del historial de Git (después de revocar)
# Herramienta: git-filter-repo (la más moderna y segura)
pip install git-filter-repo
git filter-repo --path archivo-con-secreto --invert-paths
# Esto reescribe TODO el historial: coordina con el equipo
# O eliminar un string específico de todo el historial:
git filter-repo --replace-text <(echo 'sk_live_abc123==>ELIMINADO')
# PASO 3: Forzar el push (requiere coordinación con el equipo)
git push --force-with-lease origin main
# PASO 4: Avisar a colaboradores
# Todos deben hacer git pull --rebase o clonar de nuevo
# El historial reescrito rompe los clones existentes
# PASO 5: Verificar que no quedó en ramas, tags o forks
# En GitHub: notificar a GitHub Support si el repo es público
# y pedir que limpien la caché
Resumen
- Las variables de entorno son pares clave-valor del sistema operativo que permiten separar la configuración del código. Son el mecanismo estándar para manejar secretos, URLs de conexión y parámetros que cambian entre entornos.
- Nunca pongas contraseñas, tokens ni claves de API directamente en el código. Una vez en el historial de Git, las credenciales son difíciles de eliminar por completo.
- El archivo
.envcontiene la configuración local de desarrollo. Debe estar en.gitignorey nunca subirse al repositorio. En su lugar, sube.env.examplecon los nombres de las variables pero sin valores reales. - En Python, usa
python-dotenvpara cargar el.envyos.environpara leer las variables. Con FastAPI,pydantic-settingsañade validación de tipos automática. - En Node.js, usa
dotenvo la opción nativa--env-filede Node 20+. Centraliza la configuración en un móduloconfig.jsque valida las variables obligatorias al arrancar. - En producción, define las variables directamente en la plataforma (Heroku, Railway, Render, Docker) en lugar de usar archivos
.env. - Si subes un secreto a Git por error: revócalo inmediatamente, luego limpia el historial con
git-filter-repo. Asume siempre que el secreto está comprometido desde el momento en que apareció en el repositorio.
No hay comentarios todavía. Sé el primero en compartir tu opinión.