Variables de Entorno
Cómo leer, establecer y gestionar variables de entorno de forma segura en Python, JavaScript y Java.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
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. Usa
exporten Bash osetxen Windows para persistirlas entre sesiones.
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: Documenta las variables requeridas sin valores reales. 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. Usagit filter-repoo BFG Repo-Cleaner para eliminarlo si ya lo commiteaste. - 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. Usa archivos de config JSON o YAML para configuración anidada, y cárgalos desde una ruta especificada por una variable de entorno.
- 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 stack traces y respuestas de error nunca deben incluir contraseñas de base de datos o claves API. 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.
Preguntas frecuentes
P: ¿Puedo usar variables de entorno en el navegador?
R: Solo en build time mediante sustitución del bundler. Nunca expongas secretos del servidor en código client-side. Usa variables públicas con el prefijo de tu framework (ej. VITE_, NEXT_PUBLIC_, REACT_APP_). El navegador no tiene acceso al entorno del servidor.
P: ¿Qué es el principio de configuración de 12-Factor App? R: Almacena la configuración en variables de entorno. Esto mantiene código y config separados, haciendo la app deployable a cualquier entorno sin cambios de código. La misma imagen Docker puede ejecutarse en dev, staging y prod con diferentes variables.
P: ¿Cómo gestiono secretos en un contenedor Docker?
R: Pásalos en runtime con flags -e, Docker secrets o móntalos como archivos. Nunca incluyas secretos en la imagen. Un registro de imágenes comprometido expondría cada secreto embebido durante el build.
P: ¿Cuál es la diferencia entre .env y exports de shell?
R: Los archivos .env son cargados por el código de la aplicación al inicio y solo afectan a ese proceso. Los exports de shell (export VAR=value) afectan la sesión de shell actual y todos los procesos hijos. Usa .env para settings por proyecto y exports de shell para herramientas globales.
P: ¿Debería validar variables de entorno en código o usar una librería de schema?
R: Ambos enfoques funcionan. Para proyectos pequeños, la validación manual al inicio está bien. Para aplicaciones más grandes, librerías de schema como envalid (Node), pydantic-settings (Python) o @ConfigurationProperties de Spring (Java) proporcionan type safety, defaults y validación automática.
¿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
Mejores Prácticas Adicionales
- Usa objetos de config tipados. Envuelve variables de entorno en una clase o schema tipado. Esto previene bugs de string-typed y centraliza defaults:
class Config:
PORT: int = int(os.getenv("PORT", "8080"))
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
config = Config()
- Nunca loguees secretos completos. Redacta valores sensibles en la salida de logs:
import re
def redact(value, visible_chars=4):
if not value:
return "***"
return value[:visible_chars] + "***"
def log_config(config):
safe = {
"DATABASE_URL": redact(config.database_url),
"API_KEY": redact(config.api_key),
"PORT": config.port,
}
print(f"Config: {safe}")
Errores Comunes Adicionales
- Almacenar JSON o datos complejos en env vars. Las variables de entorno son strings planos. Para config estructurada, usa un file path en una env var y carga el archivo:
# Mal: JSON complejo en env var
config = json.loads(os.getenv("APP_CONFIG")) # Frágil, difícil de debuggear
# Bien: file path en env var
config_path = os.getenv("CONFIG_PATH", "config.json")
config = json.load(open(config_path))
- No manejar variables unset en Docker. Si una env var requerida falta, el contenedor arranca con un string vacío. Valida al inicio:
const required = ["API_KEY", "DATABASE_URL"];
for (const key of required) {
if (!process.env[key]) {
console.error(`Missing required env var: ${key}`);
process.exit(1);
}
}
FAQ Adicional
¿Cómo roto secretos sin downtime?
Usa un gestor de secretos que soporte rotación automática (AWS Secrets Manager, HashiCorp Vault). Tu app debería re-fetchear secretos periódicamente o escuchar eventos de rotación. Para rotación sin downtime, usa un connection pool que pueda reconectar gracefulmente con nuevas credenciales.
¿Cuál es el tamaño máximo de una variable de entorno?
La mayoría de los SO limitan el bloque total de entorno a 32KB-128KB. Variables individuales pueden ser de hasta 8KB en Linux. Nunca almacenes payloads grandes en env vars — usa archivos o un servicio de config.
¿Cómo comparto variables de entorno entre contenedores Docker?
Usa env_file de Docker Compose o ConfigMaps/Secrets de Kubernetes. Para compartir secretos cross-service, usa un gestor de secretos como Vault o AWS Secrets Manager con control de acceso basado en IAM.
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"] Recursos Relacionados
Docker Basics
How to containerize an application, write a Dockerfile, and run containers with Docker Compose.
RecipeJWT Authentication
How to generate, validate, and refresh JSON Web Tokens for stateless API authentication.
RecipePassword Hashing
How to securely hash and verify passwords using modern algorithms across Python, JavaScript, and Java.
RecipeBackground Jobs
How to schedule and run background jobs using cron, task queues, and workers.
RecipeCLI Tool with Argument Parsing
How to build a professional command-line interface with argument parsing, flags, and subcommands.
RecipeFeature Flags
How to implement feature toggles to safely roll out, test, and rollback functionality without deploying code.
RecipeGenerate Sitemaps Live
How to build and serve live XML sitemaps from your application data, with multi-language support, pagination, and automatic lastmod dates.