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 acaben en el control de versiones.
Antes de que las variables de entorno se convirtieran en el estándar, la configuración a menudo se incrustaba en el código fuente o se guardaba en archivos XML versionados en el repositorio. Eso hacía los despliegues frágiles: un cambio de contraseña de base de datos requería un commit, rebuild y redeploy. Las variables de entorno lo resuelven externalizando cada configuración específica del entorno, de modo que el mismo artefacto compilado corre en dev, staging y producción sin tocarlo — el principio “build once, deploy many” detrás de CI/CD moderno y los contenedores.
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 escalar lecturas
- Habilitas logging de depuración 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 pruebas locales sin ensuciar 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 tiempo de ejecución.
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 convierte 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);
}
}
Archivo .env
# .env — nunca subas 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 las variables de entorno, heredadas del proceso padre (shell, systemd, Docker). Los procesos hijos no pueden modificar el entorno del padre.load_dotenv()/require('dotenv').config(): carga variables desde un archivo.enven desarrollo. No subas ese archivo al repo — es una conveniencia solo para desarrollo local.- Defaults: define defaults para lo trivial (puertos, flags de debug); nunca para secretos. Si falta una variable requerida, falla rápido al inicio con un error claro en vez de caerte después con algo críptico.
- Coerción de tipos: las variables de entorno son siempre strings — convierte a int/boolean explícitamente. En JavaScript,
"false"se evalúa como verdadero si no lo comparas. - Ámbito: las variables definidas en el shell son visibles para el proceso actual y sus hijos. Usa
exporten Bash osetxen Windows para persistirlas entre sesiones.
Variantes
Go
El os.Getenv de Go retorna un string vacío cuando la clave no existe. Usa os.LookupEnv para distinguir entre no definida y vacía:
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
# Cargar .env 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
Docker
# Dockerfile — definir defaults en build time
ENV NODE_ENV=production
ENV PORT=3000
# Sobrescribir 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
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
DATABASE_URL: postgres://db-svc:5432/mydb
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 — 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 — 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 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 sobrescribir lo específico
// 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
Lo que funciona
- Nunca subas secretos al repo: agrega
.enva.gitignoredesde el primer día. Un.envcon credenciales de producción queda en el historial de Git para siempre — borrar el archivo no lo elimina. - Publica un
.env.example: documenta las variables requeridas sin valores reales, para que un dev nuevo lo copie a.envy complete sus propias credenciales. - Valida al inicio: falla rápido cuando falte una variable requerida; una app a medias configurada falla después con errores imposibles de rastrear.
- Ámbito por entorno:
.env.development,.env.production. Muchos frameworks los cargan automáticamente segúnNODE_ENVo equivalente. - Usa un secrets manager en producción: AWS Secrets Manager, Azure Key Vault y HashiCorp Vault te dan rotación, logs de auditoría y control de acceso que los archivos planos no pueden.
- Loguea configuración, no secretos: una línea como
DATABASE_URL=***te dice que la variable está definida sin filtrar credenciales. - Prefija las variables públicas en frontend: Vite, Next.js y Create React App solo exponen
VITE_*,NEXT_PUBLIC_*oREACT_APP_*al navegador; todo lo demás queda del lado del servidor. - Rota secretos regularmente: incluso el mejor almacenamiento se puede comprometer. Pon la rotación de claves API y contraseñas en un calendario trimestral.
Errores comunes
- Subir
.envcon secretos reales: borrar el archivo no lo saca del historial de Git. Límpialo congit filter-repoo BFG Repo-Cleaner si ya pasó. - Asumir que las variables existen sin defaults: la app se cae con errores confusos. Valida las requeridas y da defaults a las opcionales.
- No validar al inicio: la configuración faltante sale a la superficie en lo profundo del call stack, lejos de la variable ausente.
- Meter datos estructurados en env vars: son strings clave-valor planos. Para config anidada, usa un archivo JSON o YAML y apunta a su ruta con una variable de entorno.
- Confundir variables de build time y runtime en frontend: los valores se incrustan en el bundle del cliente al compilar; cambiar la variable después no hace nada.
- Imprimir secretos en mensajes de error: los atacantes escanean stack traces y páginas de error públicas buscando exactamente esto.
- Reutilizar los mismos secretos en todos los entornos: dev y producción deben usar credenciales distintas — una contraseña de dev filtrada no debería abrir producción.
Tips de Rendimiento
- Lee las env vars una vez al inicio. Las llamadas repetidas a
os.getenv()tienen un 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
- No parses valores complejos en cada llamada. Parsea una vez y 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
- Carga perezosa para secretos. Obtén los secretos de AWS Secrets Manager en el primer uso y 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"]
Ver también
Preguntas frecuentes
¿Puedo usar variables de entorno en el navegador?
Solo en build time mediante sustitución del bundler — el navegador no puede ver el entorno del servidor. Nunca expongas secretos del servidor en código de cliente; usa el prefijo público de tu framework (VITE_, NEXT_PUBLIC_, REACT_APP_).
¿Qué es el principio de configuración de 12-Factor App?
Almacena la configuración en variables de entorno para que el mismo artefacto se despliegue en cualquier sitio sin cambios de código. Una imagen Docker puede correr dev, staging y prod con variables distintas.
¿Cómo gestiono secretos en un contenedor Docker?
Pásalos en runtime con flags -e, Docker secrets o como archivos montados — nunca los incrustes en la imagen. Si el registro se filtra, cada secreto embebido se filtra con él.
¿Cuál es la diferencia entre .env y exports de shell?
Los archivos .env los carga la aplicación al inicio y solo afectan a ese proceso. export VAR=value afecta toda la sesión de shell y sus procesos hijos. Usa .env por proyecto y exports para herramientas globales.
¿Valido las variables a mano o con una librería de schema?
Para proyectos pequeños, validar a mano al inicio basta. A escala, envalid (Node), pydantic-settings (Python) o @ConfigurationProperties de Spring te dan tipos, defaults y validación automática.
¿Qué pasa si falta una variable requerida?
Lo que tú decidas — así que haz que falle rápido. Lee las requeridas al inicio, lanza un error claro nombrando la clave ausente y sal antes de servir tráfico.
¿Archivos .env o secrets manager en producción?
Los archivos sirven para dev y despliegues pequeños. En producción, un secrets manager añade rotación, logs de auditoría y control de acceso — y mantiene los secretos en texto plano fuera del disco.
Recursos Relacionados
Fundamentos de Docker
Cómo containerizar una aplicación, escribir un Dockerfile y ejecutar contenedores con Docker Compose.
RecipeGestionar Secretos de Aplicaciones de Forma Segura
Cómo almacenar, rotar e inyectar API keys, contraseñas de base de datos y certificados sin hardcodearlos en código fuente o archivos de entorno.
RecipeGestión de Secretos Docker Sin Hardcodear Credenciales
Inyecta secretos en contenedores usando Docker secrets, archivos env y gestores externos sin hardcodearlos en imágenes.
RecipeAutenticación JWT
Firma, verifica y refresca JWTs en Python, JavaScript y Java — flujos access/refresh, pares de claves RS256 y revocación de tokens bien hecha.
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.
GuideGestión de Secretos: Vault y Cloud Managers
Guía práctica de gestión de secretos: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault y GCP Secret Manager con rotación, control de acceso e integración CI/CD.