Skip to content
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

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 .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. Usa export en Bash o setx en Windows para persistirlas entre sesiones.

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: Documenta las variables requeridas sin valores reales. 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. Usa git filter-repo o 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

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

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

  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"]