StackPractices
beginner Por Mathias Paulenko

Variables de Entorno

Cómo leer, establecer y gestionar variables de entorno de forma segura en Python, JavaScript y Java.

Temas: devops

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 . env en 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 . env a . gitignore inmediatamente. Un solo archivo . env commiteado 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 . env y 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 en NODE_ENV o 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 . env no 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_* o REACT_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 .env con 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

  1. 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
  1. 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
  1. 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