Variables de Entorno
Cómo leer, establecer y gestionar variables de entorno de forma segura en Python, JavaScript y Java.
Visión general
Las variables de entorno son pares clave-valor establecidos fuera del código de tu aplicación, usados para configurar comportamiento sin modificar archivos fuente. Son la piedra angular de la metodología 12-Factor App y la forma estándar de gestionar secretos, claves API, URLs de base de datos y feature flags.
Separar la configuración del código hace que las aplicaciones sean portables entre entornos (dev, staging, producción) y evita que datos sensibles se commiteen al control de versiones.
Antes de que las variables de entorno se convirtieran en el estándar, la configuración a menudo se incrustaba directamente en el código fuente o se almacenaba en archivos XML versionados en repositorios. Esto hacía los despliegues frágiles: un cambio de contraseña de base de datos requería un commit de código, rebuild y redeploy. Las variables de entorno resuelven esto externalizando todas las configuraciones específicas del entorno, permitiendo que el mismo artefacto compilado se ejecute en desarrollo, staging y producción sin modificación. Este principio — conocido como “build once, deploy many” — es esencial para pipelines modernos de CI/CD y arquitecturas containerizadas.
Cuándo usarlo
Usa esta recipe cuando:
- Configuras apps por entorno (dev, staging, prod). Consulta Docker Basics para configuración de apps containerizadas.
- Almacenas secretos como claves API y credenciales de base de datos. Consulta JWT Authentication para manejo seguro de tokens.
- Habilitas o deshabilitas capacidades con feature flags. Consulta Feature Flags para gestión de toggles.
- Gestionas configuración de aplicaciones containerizadas en Docker y Kubernetes. Consulta Docker Compose Local Dev para orquestación local de contenedores.
- Evitas valores hard-codeados en el código fuente
- Compartes configuración entre microservicios sin un servidor de configuración central
- Cambias endpoints de base de datos entre primaria y réplica para escalado de lecturas
- Habilitas debug logging o profiling solo en entornos específicos
Solución
Python
El os.getenv de Python lee variables de entorno con un default opcional. El paquete python-dotenv carga variables desde un archivo .env en desarrollo, lo cual es conveniente para testing local sin polucionar el entorno de tu shell.
import os
from dotenv import load_dotenv
# Cargar desde archivo .env (solo en dev)
load_dotenv()
# Leer variables
api_key = os.getenv('API_KEY')
db_url = os.getenv('DATABASE_URL', 'sqlite:///default.db') # con default
port = int(os.getenv('PORT', '8080'))
debug = os.getenv('DEBUG', 'false').lower() == 'true'
print(f"API_KEY={api_key}, PORT={port}, DEBUG={debug}")
JavaScript (Node.js)
Node.js expone variables de entorno a través de process.env. El paquete dotenv carga un archivo .env al inicio de la aplicación, pero solo funciona en Node — los entornos de navegador no tienen acceso a process.env en runtime.
require('dotenv').config(); // Cargar archivo .env
const apiKey = process.env.API_KEY;
const dbUrl = process.env.DATABASE_URL || 'mongodb://localhost:27017/default';
const port = parseInt(process.env.PORT || '8080', 10);
const debug = process.env.DEBUG === 'true';
console.log(`API_KEY=${apiKey}, PORT=${port}, DEBUG=${debug}`);
Java
El System.getenv() de Java devuelve un mapa inmutable del entorno del proceso. Usa getOrDefault para proporcionar valores de fallback para configuración opcional, y parsea strings a los tipos apropiados explícitamente.
public class Config {
public static void main(String[] args) {
String apiKey = System.getenv("API_KEY");
String dbUrl = System.getenv().getOrDefault("DATABASE_URL", "jdbc:mysql://localhost/default");
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
boolean debug = Boolean.parseBoolean(System.getenv().getOrDefault("DEBUG", "false"));
System.out.println("API_KEY=" + apiKey + ", PORT=" + port + ", DEBUG=" + debug);
}
}
Ejemplo de archivo .env
# .env — nunca commitear este archivo al control de versiones
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
API_KEY=sk-live-xxxxxxxxxxxx
PORT=3000
DEBUG=true
Agrega .env a .gitignore:
.env
.env.local
.env.*.local
Explicación
os.environ/process.env/System.getenv(): Acceso en tiempo de ejecución a variables de entorno. Estas se heredan del proceso padre (shell, systemd, Docker) y no pueden ser modificadas por procesos hijos de forma que afecten al padre.load_dotenv()/require('dotenv').config(): Carga variables desde un archivo. enven desarrollo. Este archivo nunca debe ser commiteado — es una conveniencia solo para desarrollo local.- Defaults: Siempre proporciona valores por defecto sensatos para valores no sensibles. Las variables requeridas faltantes deberían hacer que la aplicación falle rápidamente al inicio con un mensaje de error claro.
- Coerción de tipos: Las variables de entorno son siempre strings — haz cast a int/boolean explícitamente. Un valor de
"false"es truthy en JavaScript si no lo comparas apropiadamente. - Scope: Las variables establecidas en el shell están disponibles para el proceso actual y sus hijos.
Lo que funciona
- Nunca commitees secretos: Agrega
. enva. gitignoreinmediatamente. Un solo archivo. envcommiteado con credenciales de producción es un liability de seguridad permanente, incluso si lo borras después — el historial de Git lo retiene para siempre. - Usa un
.env.example: Los nuevos desarrolladores pueden copiar este archivo a. envy llenar sus propias credenciales. - Valida al inicio: Falla rápido si faltan variables requeridas. No dejes que tu aplicación se ejecute en un estado parcialmente configurado que produce errores crípticos horas después.
- Scope por entorno:
. env. development,. env. production. Algunos frameworks cargan estos automáticamente basándose enNODE_ENVo equivalente. - Usa un secrets manager en producción: AWS Secrets Manager, Azure Key Vault, HashiCorp Vault. Estos proporcionan rotación, logging de auditoría y control de acceso granular que los archivos
. envno pueden igualar. - Loguea configuración (no secretos): Imprime la config cargada para debugging, pero redacta claves sensibles. Una línea de log como
DATABASE_URL=***te dice que la variable está seteada sin exponer credenciales. - Prefija variables públicas en frontend: Frameworks como Vite, Next. js y Create React App solo exponen variables
VITE_*,NEXT_PUBLIC_*oREACT_APP_*al navegador. Todo lo demás permanece del lado del servidor. - Rota secretos regularmente: incluso el mejor almacenamiento puede ser comprometido. Establece un recordatorio de calendario para rotar claves API y contraseñas de base de datos trimestralmente.
Errores comunes
- Commitear archivos
.envcon secretos reales a GitHub: Incluso si borras el archivo después, permanece en el historial de Git para siempre. - Asumir que las variables de entorno existen sin defaults: Tu aplicación se caerá con errores confusos. Siempre valida variables requeridas y proporciona defaults sensatos para las opcionales.
- No validar variables requeridas al inicio de la aplicación: La configuración faltante a menudo causa fallos profundos en el call stack que son difíciles de rastrear hasta una variable de entorno ausente.
- Usar variables de entorno para datos estructurados complejos: Las variables de entorno son strings key-value planos.
- Confundir variables de build-time y runtime en bundlers de frontend: Las variables referenciadas en código frontend se embeben en build time, no se leen en runtime. Cambiar una variable de entorno después de build no tiene efecto en el bundle del cliente.
- Imprimir secretos en mensajes de error: Los atacantes escanean logs y páginas de error públicas exactamente por este error.
- Usar los mismos secretos en todos los entornos: Desarrollo y producción deben usar credenciales diferentes. Una contraseña de base de datos de dev filtrada no debería otorgar acceso a producción.
Tips de Rendimiento
- Lee env vars una vez al inicio. Llamadas repetidas a
os.getenv()tienen overhead mínimo, pero cachear en un objeto de config es más limpio:
# Leer una vez
_config = Settings()
# Usar en todas partes
def get_db_url():
return _config.database_url
- Evita parsear valores complejos desde env vars. Parsea una vez, cachea el resultado:
# Parsear una vez
_allowed_origins = os.getenv("ALLOWED_ORIGINS", "").split(",")
# Usar lista cacheada
def is_origin_allowed(origin):
return origin in _allowed_origins
- Usa lazy loading para secretos. Fetch secretos desde AWS Secrets Manager en el primer uso, luego cachea:
import boto3
from functools import lru_cache
@lru_cache(maxsize=1)
def get_db_password():
client = boto3.client("secretsmanager")
response = client.get_secret_value(SecretId="prod/db/password")
return response["SecretString"] Preguntas frecuentes
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Go
Go's os.Getenv retorna un string y un string vacío si la clave no está presente. Usa os.LookupEnv para distinguir entre unset y vacío:
package main
import (
"fmt"
"os"
"strconv"
)
func getEnv(key, fallback string) string {
if val, ok := os.LookupEnv(key); ok {
return val
}
return fallback
}
func main() {
apiKey := os.Getenv("API_KEY")
dbURL := getEnv("DATABASE_URL", "postgres://localhost:5432/default")
port, _ := strconv.Atoi(getEnv("PORT", "8080"))
debug := getEnv("DEBUG", "false") == "true"
fmt.Printf("API_KEY=%s, DB=%s, PORT=%d, DEBUG=%v\n", apiKey, dbURL, port, debug)
}
Bash
#!/bin/bash
# Source .env file si existe
if [ -f .env ]; then
set -a
source .env
set +a
fi
# Leer variables con defaults
API_KEY="${API_KEY:-default-key}"
DB_URL="${DATABASE_URL:-postgres://localhost:5432/default}"
PORT="${PORT:-8080}"
DEBUG="${DEBUG:-false}"
echo "API_KEY=$API_KEY, PORT=$PORT, DEBUG=$DEBUG"
# Validar variables requeridas
if [ -z "$API_KEY" ]; then
echo "ERROR: API_KEY es requerida" >&2
exit 1
fi
Variables de Entorno en Docker
# Dockerfile — setear defaults en build time
ENV NODE_ENV=production
ENV PORT=3000
# Override en runtime: docker run -e PORT=8080 myapp
# Usar ARG para variables solo de build time
ARG BUILD_VERSION=latest
ENV APP_VERSION=$BUILD_VERSION
# Pasar env vars en runtime
$ docker run -e DATABASE_URL=postgres://prod-db:5432/myapp -e API_KEY=sk-live-xxx myapp:v1
# Cargar desde archivo
$ docker run --env-file .env.production myapp:v1
# Docker Compose
$ docker compose --env-file .env.production up
# docker-compose.yml
services:
app:
image: myapp:v1
environment:
- DATABASE_URL=postgres://db:5432/myapp
- API_KEY=${API_KEY}
- PORT=3000
env_file:
- .env.production
Kubernetes ConfigMap y Secrets
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DATABASE_URL: postgres://db-svc:5432/myapp
PORT: "8080"
DEBUG: "false"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
API_KEY: sk-live-xxxxxxxxxxxx
DB_PASSWORD: super-secret-password
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
template:
spec:
containers:
- name: api
image: myapp:v1
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
Validación con Schema usando pydantic-settings (Python)
from pydantic_settings import BaseSettings
from pydantic import Field, ValidationError
class Settings(BaseSettings):
api_key: str = Field(..., min_length=10)
database_url: str = Field(..., min_length=1)
port: int = Field(default=8080, ge=1, le=65535)
debug: bool = False
allowed_origins: list[str] = ["localhost"]
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
try:
settings = Settings()
print(f"Loaded: port={settings.port}, debug={settings.debug}")
except ValidationError as e:
print(f"Configuration error: {e}")
raise SystemExit(1)
Validación con Schema usando envalid (Node.js)
const { cleanEnv, str, port, bool, email } = require("envalid");
const env = cleanEnv(process.env, {
API_KEY: str({ minLength: 10 }),
DATABASE_URL: str({ default: "postgres://localhost:5432/default" }),
PORT: port({ default: 8080 }),
DEBUG: bool({ default: false }),
ADMIN_EMAIL: email({ default: "admin@example.com" }),
});
// env ahora está tipado y validado
console.log(`API_KEY set: ${env.API_KEY.length > 0}`);
console.log(`PORT: ${env.PORT}`);
Archivos .env Específicos por Entorno
# .env.development
DATABASE_URL=postgres://localhost:5432/dev_db
DEBUG=true
LOG_LEVEL=debug
# .env.staging
DATABASE_URL=postgres://staging-db:5432/staging_db
DEBUG=false
LOG_LEVEL=info
# .env.production
DATABASE_URL=postgres://prod-db:5432/prod_db
DEBUG=false
LOG_LEVEL=warning
# Python: cargar .env específico por entorno
import os
from dotenv import load_dotenv
env = os.getenv("APP_ENV", "development")
load_dotenv(f".env.{env}")
load_dotenv(".env", override=False) # Fallback, no override env-specific
// Node.js: dotenv-flow para carga jerárquica de .env
require("dotenv-flow").config();
// Carga en orden: .env.{NODE_ENV}.local > .env.{NODE_ENV} > .env.local > .env
Recursos Relacionados
Fundamentos de Docker
Cómo containerizar una aplicación, escribir un Dockerfile y ejecutar contenedores con Docker Compose.
RecipeAutenticación JWT
Cómo generar, validar y refrescar JSON Web Tokens para autenticación de APIs sin estado.
RecipeCómo hashear contraseñas (Python, JavaScript, Java)
Aprendé a hashear y verificar contraseñas con bcrypt, Argon2 y PBKDF2. Ejemplos prácticos en Python, JavaScript y Java, más pasos de migración y trade-offs de parámetros.
RecipeTareas en Segundo Plano (Background Jobs)
Cómo programar y ejecutar tareas en segundo plano usando cron, colas de trabajo y workers.
RecipeParsear y Validar Configuración YAML/JSON
Cómo parsear y validar archivos de configuración de aplicaciones en YAML y JSON en Python, JavaScript, Java y Go.
RecipeParseo de argumentos CLI en Python, JS, Java, Go y Rust
Construí herramientas de línea de comandos que manejen flags, argumentos posicionales, subcomandos y validación en Python, JS, Java, Go y Rust.