Respuesta corta#

Las variables de entorno son valores de configuración que viven fuera de tu código y que el sistema operativo le entrega a tu app cuando arranca el proceso. Sirven para que la misma base de código funcione en local, en staging y en producción sin cambiar una línea: solo cambian los valores.

DATABASE_URL es la convención más extendida para guardar la cadena de conexión a tu base de datos en una sola variable. La regla de oro: nunca escribas credenciales en el código ni subas tu archivo .env al repositorio. Declara las variables en el entorno de cada plataforma y léelas con process.env en Node u os.environ en Python.

En esta guía verás cómo tu app lee esas variables, qué significa cada pieza de DATABASE_URL, cómo conectar con TLS verificado en producción sin romper el entorno local, dónde guardar los secrets y qué hacer si uno se filtra.

Documentación: The Twelve-Factor App · III. Config ↗

El principio de fondo: la configuración no vive en el código#

Antes de tocar DATABASE_URL conviene entender por qué existen las variables de entorno. La idea, popularizada por la metodología Twelve-Factor App, es separar dos cosas que suelen mezclarse:

  • El código: lo que hace tu app. Es idéntico en tu laptop, en staging y en producción.
  • La configuración: los valores que cambian entre entornos, como la URL de la base de datos, las claves de API o el modo de ejecución.

Si metes la configuración dentro del código, tienes que editar y volver a desplegar cada vez que cambia un valor, y terminas con credenciales de producción escritas en archivos que cualquiera con acceso al repositorio puede leer. Las variables de entorno resuelven ambos problemas: el código pregunta cuál es la URL de la base de datos y el entorno responde con el valor correcto según dónde esté corriendo.

Una prueba útil: si pudieras publicar tu repositorio hoy mismo sin exponer ninguna credencial, tu configuración está bien separada.

Documentación: The Twelve-Factor App · III. Config ↗

Cómo tu app lee una variable de entorno#

El sistema operativo expone las variables al proceso de tu app y cada lenguaje tiene su forma de leerlas: en Node.js a través del objeto process.env y en Python mediante el mapeo os.environ.

javascript
// Node.js
const dbUrl = process.env.DATABASE_URL;
const port = Number(process.env.PORT) || 3000;

# Python
import os
db_url = os.environ.get("DATABASE_URL")      # None si no existe
port = int(os.environ.get("PORT", "3000"))
secret = os.environ["API_KEY"]               # KeyError si no existe: falla rápido
Lectura de variables en Node.js y Python. Las variables de entorno siempre llegan como texto: convierte los números explícitamente.

Fíjate en el patrón con valor por defecto para PORT: lee el valor del entorno y, si no existe, usa uno fijo. Importa porque muchas plataformas asignan el puerto dinámicamente mediante la variable PORT, y tu app debe escuchar en ese puerto, no en uno fijo.

Para valores obligatorios como DATABASE_URL o una clave de API, un valor por defecto es una mala idea: es mejor que la app falle al arrancar con un mensaje claro que arrancar a medias y fallar en la primera consulta. En Python, os.environ con corchetes lanza KeyError si la variable no existe; en Node, compruébalo tú al inicio.

Para desarrollo local, las versiones recientes de Node pueden cargar un archivo .env con la opción --env-file, y en Python es habitual usar python-dotenv. En producción no necesitas ese archivo: la plataforma inyecta las variables directamente.

Documentación: Node.js · process.env ↗ · Python · os.environ ↗

Anatomía de DATABASE_URL#

DATABASE_URL condensa todos los datos de conexión en una sola cadena con formato de URI. Desglosarla es la mejor forma de entenderla:

text
postgresql://app_user:s3cr%[email protected]:5432/app_db?sslmode=verify-full
└─ esquema ─┘ └usuario┘ └contraseña┘ └──── host ────┘ └puerto┘ └─ base ─┘ └─ parámetros ─┘
Ejemplo de URI de conexión de PostgreSQL. La contraseña real es s3cr@to; la @ va codificada como %40.
  • Esquema: el motor de base de datos, por ejemplo postgresql://, mysql:// o mongodb://.
  • Usuario y contraseña: las credenciales, separadas por dos puntos y terminadas en @.
  • Host: el dominio o IP del servidor de base de datos.
  • Puerto: dónde escucha el motor; 5432 es el predeterminado de PostgreSQL y 3306 el de MySQL.
  • Nombre de la base de datos: lo que va después de la barra.
  • Parámetros: opciones extra tras el signo ?, como sslmode para controlar el cifrado.

Un error frecuente: si la contraseña contiene caracteres con significado especial en una URI, como @, :, / o #, debes codificarlos con percent-encoding; si no, el driver interpreta mal dónde termina cada parte. Muchos paneles de proveedores ya entregan la URL codificada, pero si la armas a mano, revísalo.

La ventaja de tenerlo todo en una variable es que mover la app entre entornos o proveedores solo requiere cambiar un valor.

Documentación: PostgreSQL · Connection URIs (libpq) ↗

El mismo código en local y en producción, con TLS verificado#

El desafío práctico es que en local y en producción cambian las condiciones, sobre todo el cifrado. En desarrollo sueles conectarte a una base local sin TLS; en producción, muchos proveedores gestionados exigen conexiones cifradas. La forma limpia de manejarlo es condicionar la configuración TLS según el entorno, usando la misma DATABASE_URL:

javascript
const { Pool } = require("pg");

const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
  throw new Error("DATABASE_URL no está definida");
}

const isProduction = process.env.NODE_ENV === "production";

const pool = new Pool({
  // No pongas sslmode en esta URL: node-postgres reemplazaría el objeto ssl
  connectionString,
  ssl: isProduction
    ? {
        rejectUnauthorized: true, // verifica el certificado del servidor
        ca: process.env.DATABASE_CA_CERT, // PEM del proveedor, si usa una CA propia
      }
    : false,
});
Ejemplo con node-postgres: TLS con verificación de certificado en producción y sin TLS en local. Si tu proveedor usa una CA pública, puedes omitir ca.

Una corrección importante respecto a la versión anterior de esta guía: muchos tutoriales usan rejectUnauthorized: false para que la conexión funcione. Eso cifra el tráfico, pero acepta cualquier certificado, así que no protege contra alguien que se haga pasar por tu servidor. Lo correcto es verificar el certificado y, si tu proveedor usa su propia autoridad certificadora, pasar su certificado raíz en ca.

Otro detalle de node-postgres: si la URL incluye sslmode, sslrootcert u opciones similares, el objeto ssl que pasas en la configuración se reemplaza por lo que diga la URL. Elige un solo lugar para configurar TLS.

En libpq, la biblioteca cliente oficial de PostgreSQL, sslmode=require cifra la conexión pero, salvo que exista un certificado raíz configurado, no verifica la identidad del servidor; verify-full verifica la cadena de certificados y que el nombre del host coincida. Para producción, verify-full es la opción segura.

Y el error inverso: dejar TLS activo en local cuando tu base de desarrollo no lo soporta hace que la conexión falle con un error de SSL. Por eso se condiciona con isProduction.

Documentación: node-postgres · SSL ↗ · PostgreSQL · SSL support (sslmode) ↗

Dónde guardar los secrets (y dónde nunca)#

No todos los lugares para guardar una variable son iguales. Esta es la jerarquía:

Lugar¿Usarlo?Para qué
Panel de variables o secrets de tu plataforma de deploySíSecrets de producción, fuera del repositorio
Gestor de secrets dedicado (AWS Secrets Manager, Vault, etc.)SíEquipos grandes, rotación y auditoría
Archivo .env local no versionadoSíSolo desarrollo en tu máquina
.env.example versionado sin valoresSíDocumentar qué variables hay que definir
Valores escritos directamente en el códigoNuncaQuedan en el repositorio y en su historial
.env real subido al repositorioNuncaCualquiera con acceso al repo ve las credenciales
Variables sensibles impresas en logsNuncaLos logs se guardan, se comparten y se indexan

Tres reglas que evitan la mayoría de las filtraciones:

  • Agrega .env a tu .gitignore desde el primer commit. Una credencial que llega al historial de Git es difícil de borrar por completo.
  • No imprimas variables sensibles en logs. Un secret en un log es un secret expuesto.
  • Versiona un .env.example con los nombres de las variables pero sin valores, para que tu equipo sepa qué definir sin exponer nada.
bash
# .env.example (se versiona, sin valores reales)
DATABASE_URL=
DATABASE_CA_CERT=
API_KEY=
NODE_ENV=development

# .gitignore
.env
.env.*
!.env.example
Plantilla versionada y reglas de .gitignore: ignora cualquier .env salvo el ejemplo.

Si usas Kubernetes, ten en cuenta que los objetos Secret se guardan sin cifrar en etcd por defecto; la documentación oficial recomienda activar el cifrado en reposo y restringir el acceso con RBAC.

Documentación: Git · gitignore ↗ · AWS · What is AWS Secrets Manager ↗ · Kubernetes · Secrets ↗ · OWASP · Secrets Management Cheat Sheet ↗

Si un secret se filtró#

Si tu archivo .env aparece en git log o un secret llegó a un log compartido, considera esas credenciales comprometidas. Borrar el archivo en un commit nuevo no basta: el valor sigue en el historial y en cualquier clon o fork.

  • Rota primero: genera credenciales nuevas en la base de datos o el proveedor y actualiza el valor en el panel de tu plataforma.
  • Revoca las credenciales antiguas en cuanto el despliegue use las nuevas.
  • Después, si hace falta, limpia el historial. GitHub documenta el proceso con git-filter-repo; BFG Repo-Cleaner es otra herramienta común.
  • Activa el escaneo de secrets de tu plataforma de código, como el secret scanning de GitHub, para detectar filtraciones futuras.

El orden importa: reescribir el historial sin rotar deja la credencial válida en manos de quien ya la copió.

Documentación: GitHub · Removing sensitive data from a repository ↗ · GitHub · About secret scanning ↗

Cómo verificar que tu app lee las variables correctas#

Cuando algo no conecta, antes de tocar el código confirma qué está recibiendo realmente tu app:

  • Imprime temporalmente si la variable existe, no su valor, por ejemplo con Boolean(process.env.DATABASE_URL).
  • Verifica que el nombre coincida exactamente: en Linux, DATABASE_URL y Database_Url son variables distintas.
  • Revisa que la variable esté declarada en el entorno correcto: build-time o runtime, y en el servicio correcto si tienes varios.
  • Confirma que no haya espacios ni comillas accidentales alrededor del valor al pegarlo en el panel.
  • Si la contraseña tiene caracteres especiales, comprueba que estén codificados en la URL.

Documentación: Node.js · process.env ↗

Variables de build-time y de runtime#

Las variables de build-time están disponibles mientras se compila la app; las de runtime, mientras la app corre. Algunas plataformas inyectan solo unas u otras, así que confirma en qué fase necesita tu app cada variable.

En frameworks de frontend la diferencia es crítica. En Next.js, las variables con prefijo NEXT_PUBLIC_ se incrustan en el JavaScript del navegador durante el build; en Vite, solo las que llevan el prefijo VITE_ se exponen al cliente. Cualquier valor expuesto así es público: nunca pongas DATABASE_URL ni otro secret en una variable con esos prefijos.

Documentación: Next.js · Environment variables ↗ · Vite · Env variables and modes ↗

Preguntas frecuentes#

¿Por qué no debo escribir la contraseña de la base de datos en el código? Porque queda expuesta a cualquiera con acceso al repositorio y a su historial, y te obliga a redesplegar para cambiarla. Con variables de entorno puedes rotarla sin tocar el código.

¿Qué significa sslmode=require al final de DATABASE_URL? Obliga a que la conexión vaya cifrada. En PostgreSQL no verifica por sí solo la identidad del servidor; para eso usa verify-full con el certificado raíz del proveedor.

¿Por qué mi conexión falla en producción pero funciona en local? La causa más común es TLS: tu base local no lo requiere y el proveedor de producción sí, o el certificado no se puede verificar. Condiciona la configuración TLS según el entorno y revisa el certificado raíz.

¿Debo subir mi archivo .env al repositorio? No. Agrégalo a .gitignore y versiona en su lugar un .env.example con los nombres de las variables sin valores.

Resumen: DATABASE_URL sin exponer credenciales#

Gestionar DATABASE_URL en producción se reduce a tres acciones: mantener la cadena de conexión fuera del código, declararla en el entorno de tu plataforma y validar al arrancar que la app la recibe y puede conectar con TLS verificado.

  • Valida que la variable existe al arrancar. Un fallo temprano con un mensaje claro ahorra depuración.
  • Separa entornos por valor, no por código. Una DATABASE_URL para local, otra para staging y otra para producción; el código solo lee la variable.
  • Rota las credenciales tras cualquier filtración. Borrar el archivo no basta.

La mayoría de las plataformas modernas de despliegue, incluidas las principales nubes, inyectan variables de entorno de forma nativa, así que no necesitas archivos .env en producción. Tu código sigue igual en local, staging y producción; solo cambia el valor que el entorno le entrega.

Documentación: The Twelve-Factor App · III. Config ↗

Fuentes y alcance

Documentación consultada el 25 de septiembre de 2026. Los ejemplos y criterios de decisión son propuestas editoriales; adáptalos al contrato de tu aplicación y valídalos en tu entorno de pruebas autorizado.

Del diseño a la decisión

Compara soluciones cloud

Revisa tarifas, límites, condiciones y fuentes de cada opción.

Abrir comparador