Prioridad de variables de entorno en Docker: por qué tu configuración nunca es la que crees

¿Te fue útil?

Despliegas un contenedor. Arranca. Está vivo. Pero no se comporta. El registro indica que se conecta a la base de datos equivocada, usa el nivel de logs incorrecto o enlaza el puerto equivocado. Te quedas mirando tu docker-compose.yml como si te hubiera mentido a la cara.

Probablemente lo hizo—accidentalmente, por la precedencia. Docker y Docker Compose tienen múltiples capas que pueden definir “la misma” variable de entorno, y la ganadora rara vez es la que suponías. La solución no es heroica. Es aprender las reglas y luego instrumentar la verdad.

El modelo mental: tres problemas distintos con “variables de entorno”

La mayor parte de la confusión con las variables de entorno en Docker viene de mezclar tres mecanismos separados que sólo parecen relacionados:

1) Entorno en tiempo de ejecución del contenedor (lo que realmente ve el proceso)

Este es el conjunto de variables dentro del contenedor en ejecución, visible para PID 1 y similares vía env o /proc/1/environ. Proviene de la configuración del contenedor de Docker, que a su vez puede obtenerse de valores por defecto de la imagen, Compose, flags de la CLI y otras entradas.

2) Interpolación en el archivo Compose (lo que Compose sustituye en el YAML)

Compose también usa variables de entorno en tu host para reemplazar marcadores ${VAR} en el YAML. Esa sustitución ocurre antes de crear cualquier contenedor. No es lo mismo que fijar el entorno del contenedor en tiempo de ejecución.

Aquí es donde la gente se quema: ponen environment: en Compose y asumen que eso también afectará a cualquier ${VAR} en otra parte del archivo. No lo hace.

3) Capas de configuración de la aplicación (lo que tu app decide respetar)

Incluso después de que Docker lo haga bien, tu aplicación puede sobrescribirlo: archivos de configuración, flags, valores por defecto, convenciones del framework o una librería que lee variables con un prefijo, ignora valores vacíos o trata "false" como verdadero porque no está vacío. Docker no puede salvarte de eso.

Si quieres un eslogan para producción: las variables de entorno en Docker no son “una configuración”. Son una entrada. Las entradas tienen reglas de precedencia. Las entradas se ignoran. Las entradas derivan.

Hechos e historia: cómo llegamos a este desorden

  • Docker no inventó la configuración basada en variables de entorno; la “app de 12 factores” popularizó las env vars a principios de los 2010, y los contenedores hicieron que pareciera universal.
  • ENV en Dockerfiles precede a Compose; los autores de imágenes pusieron valores por defecto mucho antes de que la mayoría de equipos estandarizara en archivos Compose para despliegue.
  • La sustitución de variables en Compose se modeló tras hábitos de shell: ${VAR}, valores por defecto y “tomar del entorno actual”. Genial para conveniencia en desarrollo, terrible para repetibilidad.
  • Las implementaciones tempranas de Compose cargaban un .env por defecto automáticamente, y ese comportamiento se volvió memoria muscular—aun cuando los equipos después separaron archivos .env por entorno.
  • Docker tiene dos conceptos de “env file”: el --env-file de la CLI y el env_file: de Compose. Se parecen, no son intercambiables y no se comportan idénticamente en casos límite.
  • La especificación de imágenes OCI guarda Env en la configuración de la imagen; ENV en un Dockerfile se convierte en metadatos horneados en la imagen y participa en la fusión en tiempo de ejecución.
  • La gestión de secretos se volvió mainstream tras fugas por env vars; las env vars son fáciles de volcar, registrar o exponer por endpoints de depuración. “Conveniente” no es lo mismo que “seguro”.
  • Compose evolucionó hacia una especificación; distintas implementaciones (el plugin docker compose vs el antiguo docker-compose) históricamente discreparon en ciertos comportamientos. Los bordes afilados están mayormente limados ahora, pero las costumbres legacy permanecen.

Una verdad operativa no ha cambiado: cuando apilas múltiples capas que pueden definir “el mismo” valor, acabarás enviando el equivocado. No porque seas descuidado. Porque eres humano y el sistema acepta entradas conflictivas sin quejarse.

Mapas de precedencia: Docker run, Dockerfile, Compose e interpolación

Dockerfile ENV vs sobrescrituras en tiempo de ejecución

Los autores de imágenes fijan valores por defecto con ENV en el Dockerfile. Estos se convierten en Config.Env de la imagen. En tiempo de ejecución, Docker mezcla variables de entorno desde múltiples orígenes. La regla que debes memorizar:

Los ajustes en tiempo de ejecución sobrescriben los valores por defecto de la imagen.

Así que si la imagen contiene ENV LOG_LEVEL=info y ejecutas:

cr0x@server:~$ docker run --rm -e LOG_LEVEL=debug alpine:3.20 env | grep LOG_LEVEL
LOG_LEVEL=debug

Precedencia de docker run (vista práctica)

Cuando arrancas un contenedor con docker run, el entorno efectivo es básicamente:

  1. Valores por defecto de la imagen (ENV en el Dockerfile)
  2. Env desde --env-file (si se usa)
  3. Flags explícitos -e KEY=VALUE que sobrescriben valores anteriores

Hay matices (como -e KEY que significa “tomar el valor del entorno del cliente”), pero operativamente: lo explícito vence a lo implícito.

Compose son dos juegos de precedencia distintos a la vez

Compose lo empeora porque juega dos juegos:

  • Interpolar variables en el archivo Compose (lado host): resolución de ${VAR}.
  • Definir el entorno en tiempo de ejecución para el servicio (lado contenedor): environment:, env_file:, además de lo que ya está en la imagen.

Precedencia de interpolación en Compose (lado host)

Para reemplazar ${VAR} en el YAML, Compose generalmente sigue esta intuición:

  1. Variables desde tu entorno de shell (el entorno donde ejecutas docker compose)
  2. Variables desde el archivo de proyecto .env (si está presente en el directorio de trabajo / directorio del proyecto)
  3. Valores por defecto en la expresión, como ${VAR:-default}

Si solo recuerdas una cosa: environment: no alimenta la interpolación. Si escribes:

cr0x@server:~$ cat docker-compose.yml
services:
  api:
    image: alpine:3.20
    environment:
      DB_HOST: db
    command: ["sh", "-lc", "echo ${DB_HOST}"]

Ese ${DB_HOST} se resuelve en el host, no dentro del contenedor. Si tu host no tiene DB_HOST establecido (y tu .env tampoco), imprimirás una cadena vacía o generarás una advertencia según la versión/configuración de Compose.

Precedencia del entorno en tiempo de ejecución de Compose (lado contenedor)

Para el entorno que termina dentro del contenedor, el orden habitual de ganadores es:

  1. Valores por defecto de la imagen ENV
  2. Variables cargadas vía env_file:
  3. Variables establecidas en environment: que sobrescriben env_file
  4. Algunas implementaciones también permiten sobrescrituras por CLI en docker compose run -e, que ganan sobre el archivo

Además: si especificas múltiples entradas de env_file, los archivos posteriores sobrescriben a los anteriores. Eso es útil para el apilamiento. También es cómo accidentalmente mandas configuración de staging a producción porque un archivo se reordenó en un diff.

Nulo, vacío y “presente pero en blanco”

Hay una diferencia molesta entre:

  • No establecido: la variable no existe en el entorno
  • Vacío: la variable existe con valor ""
  • Cadena literal “null”: existe y vale null (común al templatear YAML)

El YAML de Compose puede expresar valores vacíos de formas que parecen inocentes:

  • FOO: (vacío)
  • FOO: "" (cadena vacía explícita)

Las apps suelen tratar “vacío pero presente” como “configurado”, llevando a comportamientos erróneos. Para bases de datos, nombres de host vacíos pueden resolverse a localhost o usar valores por defecto en librerías. El contenedor está bien. La app está “ayudando”.

Una cita para mantenerte honesto

La esperanza no es una estrategia. — General Gordon R. Sullivan

La precedencia de variables de entorno es donde la esperanza va a morir. Instrumenta en su lugar.

Donde nace la deriva de configuración: los recovecos que duelen en producción

Problema: .env no es el entorno de tu contenedor

El archivo .env usado por Compose para interpolación es una característica de conveniencia. No se inyecta automáticamente en el contenedor a menos que lo referencias explícitamente con env_file: o mapees valores en environment:.

Por eso ocurre el “funcionaba en mi portátil”: el portátil tiene un .env en el directorio del proyecto, CI no lo tiene y producción usa un directorio de trabajo diferente o ejecuta Compose desde un script envoltorio.

Problema: “Cambié la variable, ¿por qué el contenedor en ejecución no cambió?”

Porque los contenedores no son shells. Actualizar un archivo Compose no parchea en vivo el entorno de un contenedor existente. Debes recrear el contenedor (o al menos reiniciarlo con recreación, dependiendo de lo que cambió).

Problema: Healthchecks y sidecars ven un mundo distinto

Los comandos de healthcheck se ejecutan dentro del contenedor, así que ven el entorno del contenedor. Pero si templateaste el comando del healthcheck con interpolación del lado host, puedes haber inyectado el valor equivocado al crear. La comprobación pasa, luego el tráfico en producción falla. Mi género favorito.

Problema: Variables de proxy y valores por defecto “útiles”

HTTP_PROXY, NO_PROXY y similares se establecen con frecuencia en portátiles corporativos, agentes de construcción e incluso unidades systemd del demonio Docker. Influyen silenciosamente en el comportamiento de build y runtime. Acabas con contenedores que sólo alcanzan Internet en ciertos entornos y nadie sabe por qué.

Chiste #1: Las variables de entorno son como los chismes de oficina: se esparcen por todas partes, rara vez están documentadas y siempre eligen el peor momento para ser ciertas.

Problema: Tu app tiene su propia precedencia

Patrones comunes:

  • Frameworks que priorizan archivos de configuración sobre env vars a menos que esté activado un interruptor “usar env”.
  • Librerías que leen DATABASE_URL si está presente, o en caso contrario leen DB_HOST/DB_USER, etc.
  • Apps que tratan 0, false y no de forma inconsistente.

En producción no quieres “magia”. Quieres un contrato de configuración explícito: qué variable gana y cómo validarla al inicio.

Tareas prácticas: comandos que te dicen qué es real (y qué hacer después)

No depuras la precedencia leyendo más intensamente el YAML. La depuras preguntándole al runtime qué hizo realmente. Abajo están tareas prácticas que he usado en sistemas reales, con comandos, salidas representativas y la decisión que tomarás.

Tarea 1: Inspeccionar el entorno efectivo del contenedor (verdad rápida)

cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api-1 | sort | sed -n '1,10p'
DB_HOST=db-prod
LOG_LEVEL=info
PORT=8080
TZ=UTC

Qué significa: Estas son las env vars registradas en la configuración del contenedor. Esto es lo que obtiene el proceso al arrancar.

Decisión: Si el valor es erróneo aquí, deja de culpar a la aplicación. Arregla Compose/flags de run/valores por defecto de la imagen y recrea el contenedor.

Tarea 2: Ver qué ve realmente el proceso (por si el entrypoint muta el entorno)

cr0x@server:~$ docker exec api-1 sh -lc 'tr "\0" "\n" < /proc/1/environ | sort | grep -E "DB_HOST|LOG_LEVEL|PORT"'
DB_HOST=db-prod
LOG_LEVEL=info
PORT=8080

Qué significa: El entorno del PID 1. Si esto difiere de docker inspect, algo dentro del contenedor lo cambió (script de entrypoint, supervisor, etc.).

Decisión: Si PID 1 difiere, audita scripts de entrypoint y herramientas de arranque. Es un problema de la app/imagen, no de Compose.

Tarea 3: Ver la configuración Compose completamente renderizada (interpolación resuelta)

cr0x@server:~$ docker compose config | sed -n '/services:/,/networks:/p' | sed -n '1,80p'
services:
  api:
    command:
    - sh
    - -lc
    - ./start-api
    environment:
      DB_HOST: db-prod
      LOG_LEVEL: info
    image: myorg/api:1.8.4

Qué significa: Esta es la interpretación final de Compose, después de fusionar archivos y resolver ${VAR}.

Decisión: Si docker compose config muestra el valor equivocado, tus entradas de interpolación están mal (env del shell, .env, defaults) o tus archivos de override no son lo que crees.

Tarea 4: Listar contenedores y confirmar que estás depurando el correcto

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES     IMAGE              STATUS          PORTS
api-1     myorg/api:1.8.4    Up 2 hours      0.0.0.0:8080->8080/tcp
db-1      postgres:16        Up 2 hours      5432/tcp

Qué significa: Nombres, imágenes, tiempo activo, puertos. Los bugs de “config equivocada” suelen ser bugs de “contenedor equivocado”.

Decisión: Si ves múltiples contenedores con nombres similares, confirma el nombre del proyecto y el contexto; podrías estar inspeccionando el stack de la semana pasada.

Tarea 5: Verificar en qué proyecto Compose estás operando

cr0x@server:~$ docker compose ls
NAME            STATUS              CONFIG FILES
billing         running(6)          /srv/billing/docker-compose.yml
billing-dev     running(6)          /home/cr0x/billing/docker-compose.yml

Qué significa: Los nombres de proyecto de Compose forman parte de la identidad. Mismo YAML, proyecto distinto, contenedores distintos.

Decisión: Si estás en el proyecto equivocado, detente. Cambia de directorio, usa -p o limpia duplicados.

Tarea 6: Comprobar si el contenedor fue recreado tras cambios de configuración

cr0x@server:~$ docker inspect -f '{{.Name}} {{.Created}}' api-1
/api-1 2026-01-03T08:12:54.123456789Z

Qué significa: Marca temporal de creación. Si editaste la configuración a las 09:00 y el contenedor fue creado a las 08:12, no cambió nada.

Decisión: Recréalo: docker compose up -d --force-recreate (o al menos up -d si Compose detecta una diferencia).

Tarea 7: Confirmar qué archivos env se aplican (y en qué orden)

cr0x@server:~$ docker compose config --services
api
db
cr0x@server:~$ grep -nE 'env_file|environment' -n docker-compose.yml
14:    env_file:
15:      - ./env/common.env
16:      - ./env/prod.env
17:    environment:
18:      LOG_LEVEL: info

Qué significa: Múltiples env files implican apilamiento. El posterior gana. environment: los sobrescribe a ambos.

Decisión: Si necesitas que un valor sea verdaderamente autoritativo, ponlo en environment: (o en un único archivo env final) y aplica orden de archivos controlado.

Tarea 8: Detectar valores de interpolación del lado host (qué Compose está tomando)

cr0x@server:~$ env | grep -E '^DB_HOST=|^LOG_LEVEL='
LOG_LEVEL=debug

Qué significa: Tu shell ya tiene LOG_LEVEL=debug. Si tu Compose usa ${LOG_LEVEL}, acabas de sobrescribir la configuración de producción con el historial de tu terminal.

Decisión: Ejecuta Compose con un entorno limpio para operaciones de producción, o establece explícitamente las vars requeridas en un archivo controlado.

Tarea 9: Mostrar qué variables faltan durante la interpolación (atrapar blancos silenciosos)

cr0x@server:~$ docker compose config 2>&1 | grep -i warning
WARNING: The "DB_PASSWORD" variable is not set. Defaulting to a blank string.

Qué significa: Compose sustituyó una cadena vacía. Tu YAML ahora es válido pero tu sistema no lo está.

Decisión: Trata esto como un fallo de despliegue. Haz que CI falle en variables ausentes, o usa patrones de variables requeridas en tus plantillas.

Tarea 10: Confirmar valores por defecto de la imagen (ENV horneado en la imagen)

cr0x@server:~$ docker image inspect myorg/api:1.8.4 -f '{{json .Config.Env}}'
["PORT=8080","LOG_LEVEL=warn","TZ=UTC"]

Qué significa: La imagen trae LOG_LEVEL=warn. Si pensabas “no establecido significa default”, el autor de la imagen ya decidió.

Decisión: O sobrescribes explícitamente en Compose, o quitas el valor por defecto de la imagen si causa sorpresas (preferible: documentación + configuración explícita).

Tarea 11: Verificar si una variable está vacía vs no establecida (dentro del contenedor)

cr0x@server:~$ docker exec api-1 sh -lc 'if printenv OPTIONAL_FLAG >/dev/null 2>&1; then echo "present:[$OPTIONAL_FLAG]"; else echo "unset"; fi'
present:[]

Qué significa: La variable existe pero está vacía. Muchas apps tratan eso como “configurado” y caen en rutas de código extrañas.

Decisión: Si vacío debería comportarse como no establecido, no lo establezcas. Elimínalo de environment: o de tus archivos env.

Tarea 12: Encontrar la fuente de un valor comparando config renderizada vs runtime

cr0x@server:~$ docker compose config --format json | jq -r '.services.api.environment'
{
  "DB_HOST": "db-prod",
  "LOG_LEVEL": "info"
}
cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api-1 | grep -E 'DB_HOST|LOG_LEVEL'
DB_HOST=db-prod
LOG_LEVEL=info

Qué significa: Compose y el runtime coinciden. Si el comportamiento sigue siendo erróneo, la app está sobreescribiendo la configuración o la está parseando mal.

Decisión: Cambia la investigación a la aplicación: logs de arranque, endpoints de volcado de config, precedencia de librerías.

Tarea 13: Confirmar qué Compose piensa que cambió (evitar despliegues nulos)

cr0x@server:~$ docker compose up -d
[+] Running 0/0

Qué significa: Compose no vio nada que hacer. Tu cambio de env puede no estar en la definición del servicio (o solo está en la interpolación pero no cambió la salida).

Decisión: Usa --force-recreate o recrea explícitamente el servicio afectado después de confirmar que la config renderizada realmente cambió.

Tarea 14: Detectar herencia accidental de proxy (trampa clásica corporativa)

cr0x@server:~$ docker exec api-1 sh -lc 'env | grep -i proxy'
HTTP_PROXY=http://proxy.corp:3128
NO_PROXY=localhost,127.0.0.1,db

Qué significa: El contenedor está usando un proxy. Eso puede romper llamadas entre servicios, validación de certificados y aumentar latencias.

Decisión: Si esto no debería estar presente, anula o limpia explícitamente las vars de proxy en Compose para cargas de producción.

Guion de diagnóstico rápido

Este es el orden que minimiza el tiempo hasta la verdad cuando un contenedor está “configurado mal”. No improvises. Usa el embudo.

Primero: identifica la verdad en tiempo de ejecución

  1. Confirma el contenedor objetivo: docker ps y verifica nombre/imagen/tiempo activo.
  2. Inspecciona el entorno efectivo: docker inspect ... .Config.Env.
  3. Revisa el entorno del PID 1: docker exec ... /proc/1/environ.

Si el entorno en runtime es erróneo, estás en el terreno de Docker/Compose. Si el entorno en runtime es correcto, estás en terreno de la aplicación.

Segundo: confirma la intención renderizada de Compose

  1. Renderiza la configuración: docker compose config.
  2. Revisa las entradas de interpolación: tu env del shell y tu .env del proyecto.
  3. Revisa el apilamiento: múltiples archivos compose, orden de env_file, overrides de environment.

Tercero: confirma que el cambio realmente se desplegó

  1. Tiempo de creación: docker inspect ... .Created.
  2. Recrea si es necesario: docker compose up -d --force-recreate para el servicio.
  3. Verifica de nuevo: inspecciona env después de la recreación.

Chiste #2: En Docker, la única configuración consistente es la que no pretendías sobrescribir.

Tres mini-historias corporativas (anonimizadas, técnicamente reales)

Incidente: la suposición equivocada (“Compose env lo sobrescribe todo, ¿verdad?”)

Una empresa mediana ejecutaba una API de pagos en Docker Compose en un puñado de hosts VM. Su flujo de trabajo era limpio en el papel: un docker-compose.yml base más un archivo de override por entorno. La aplicación aceptaba configuración vía env vars. Clásico.

Un despliegue del viernes aplicó un cambio: una nueva env var PAYMENTS_PROVIDER_TIMEOUT_MS. El ingeniero la puso en environment: del override de producción. El servicio seguía agotando tiempo bajo carga—luego empezó a reintentar agresivamente, provocando límites de tasa aguas arriba.

El equipo asumió que el valor “no se aplicó”. Lo aumentaron de nuevo. Mismo comportamiento. El problema real era que la imagen ya tenía ENV PAYMENTS_PROVIDER_TIMEOUT_MS=2000 y la aplicación tenía un archivo de configuración horneado en la imagen que tenía prioridad sobre las env vars a menos que USE_ENV_CONFIG=true estuviera establecido. En staging ese flag se fijaba vía un export en el shell de un desarrollador e interpolado en el archivo de override. En producción no estaba. Compose sustituyó un blanco. El contenedor arrancó con el comportamiento por defecto: ignorar la configuración por env.

Depuraron el proveedor, la red y la base de datos antes de que alguien ejecutara docker compose config y viera la advertencia de variable faltante. La solución fue aburrida: hacer USE_ENV_CONFIG explícito en la definición del servicio en Compose y hacer que el deploy falle si falta. Luego reconstruyeron la imagen para dejar de enviar un archivo de configuración ambiguo.

Conclusión del postmortem: no trates “la variable de entorno existe” como “la app usa env”. Son dos contratos diferentes, y uno de ellos nunca se escribió.

Optimización que salió mal: “Vamos a desduplicar config con un env file compartido”

Un equipo de una gran empresa se cansó de repetir configuración en 20 servicios. Introdujeron un env/common.env compartido e lo incluyeron vía env_file: en todas partes. Luego añadieron env/prod.env, env/stage.env, etc. El ruido en los diffs bajó. La gente celebró. Así empieza.

Tres meses después, se añadió un servicio nuevo. Alguien copió un stanza de servicio existente y olvidó incluir env/prod.env. El servicio arrancó con los valores por defecto de la imagen y de common.env. “Funcionó”, pero habló con el clúster de cache de staging porque CACHE_HOST vivía en prod.env para la mayoría de servicios y en common.env para un servicio legacy. Ambos valores eran hostnames plausibles. No hubo crash inmediato. Solo rutas de datos equivocadas.

Intentaron “arreglarlo” moviendo más cosas a common.env. Eso amplió el radio de impacto. Ahora una inclusión u omisión cambiaba el comportamiento en múltiples entornos. Introdujeron “deriva de entorno por inclusión de archivo”. No es un typo; es una capa faltante.

La recuperación fue dejar de fingir que un archivo env sirve a todos los servicios. Mantuvieron un archivo compartido, pero limitado a valores globales no riesgosos (zona horaria, formato de logs, toggles comunes) y exigieron que cada servicio tuviera un archivo explícito por entorno. CI validó que cada servicio tuviera las fuentes de env esperadas. La desduplicación se quedó, pero con guardrails.

Aburrido pero correcto: pin, render, verify (y salvó el día)

Un equipo más pequeño ejecutaba una plataforma de soporte al cliente donde la caída se veía en minutos. Tenían una costumbre que parecía paranoica: cada deploy producía un artefacto que contenía la configuración Compose totalmente renderizada (docker compose config) y la lista final de env vars por servicio, y lo guardaban junto con los metadatos del build.

Una tarde, la API empezó a rechazar peticiones porque pensaba que estaba en “modo mantenimiento”. Ese modo lo controlaba MAINTENANCE=true. Nadie lo había puesto. Nadie admitía haberlo puesto. Slack hacía lo suyo.

Compararon el artefacto actual con el anterior. La config renderizada mostraba claramente que MAINTENANCE=true se había interpolado desde el entorno del host durante un despliegue manual de hotfix. El ingeniero lo había exportado antes para una prueba local y lo olvidó. Compose obedientemente lo sustituyó en el YAML que usaba ${MAINTENANCE} por conveniencia.

La solución tomó cinco minutos: volver a ejecutar el despliegue con un entorno limpio y eliminar la interpolación del host para esa bandera. La lección quedó porque era medible: su práctica “paranoica” transformó un misterio vago en una única línea de diff. Nada heroico. Solo evidencia.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: la variable es correcta en docker-compose.yml, pero el contenedor tiene otro valor

Causa raíz: Cambiaste el YAML pero no recreaste el contenedor, o estás mirando otro proyecto Compose.

Solución: Revisa docker compose ls, confirma la hora de creación del contenedor y luego docker compose up -d --force-recreate api.

2) Síntoma: ${VAR} queda en blanco aunque lo pusiste bajo environment:

Causa raíz: Confundir interpolación del lado host con el entorno en tiempo de ejecución del contenedor. Compose interpola desde el shell y .env, no desde environment:.

Solución: Mueve el valor al entorno del host (controlado), o deja de interpolar y usa literales. Valida con docker compose config.

3) Síntoma: el valor difiere entre staging y prod aun con los mismos archivos Compose

Causa raíz: Entorno de shell distinto en el momento del deploy (agente CI vs shell humano), o un .env diferente en el directorio de trabajo.

Solución: Despliega desde un entorno controlado. Evita depender de exports del shell del desarrollador. Almacena las vars de interpolación requeridas en un archivo explícito usado por el job de deploy.

4) Síntoma: la app se comporta como si una opción estuviera “habilitada” aun cuando pusiste false

Causa raíz: La app parsea booleans mal (“cadena no vacía es truthy”), o usa un nombre de variable distinto al que crees.

Solución: Confírmalo volcándo la config efectiva al inicio de la app. Usa parsing estricto en la app. Prefiere 0/1 si la app es descuidada.

5) Síntoma: secretos aparecen en logs o bundles de soporte

Causa raíz: Secretos pasados vía env vars son fáciles de volcar con env, endpoints de debug o dumps de crash.

Solución: Usa secretos de Docker (o secretos montados como archivos) y pasa rutas, no valores. Como mínimo, redacta y asegura la información diagnóstica.

6) Síntoma: peticiones pasan aleatoriamente por un proxy o fallan solo en algunos hosts

Causa raíz: Vars de proxy heredadas del host, unidad systemd o runner CI.

Solución: Establece/limpia explícitamente HTTP_PROXY/NO_PROXY en Compose para producción. Verifica dentro del contenedor.

7) Síntoma: Compose advierte “variable not set, defaulting to blank string”, pero el despliegue aún tiene éxito

Causa raíz: Entradas de interpolación requeridas faltan, y tu pipeline no trata las advertencias como fallos.

Solución: Bloquea despliegues si hay variables faltantes analizando la salida de docker compose config en CI, o validando una lista de vars requeridas antes de ejecutar Compose.

8) Síntoma: una variable de env_file no parece aplicarse

Causa raíz: Está siendo sobrescrita por environment:, por un env_file posterior, o por docker compose run -e.

Solución: Renderiza la config, revisa el orden y decide qué capa es la autoritativa. Hazlo explícito.

Listas de verificación / plan paso a paso

Checklist: hacer que la precedencia de env vars sea aburrida (el objetivo)

  1. Deja de usar interpolación del host para ajustes en tiempo de ejecución a menos que tengas un entorno de despliegue controlado. Si cambia, debe cambiar en configuración versionada.
  2. Prefiere environment: explícito para valores críticos del servicio (endpoints de BD, feature flags que pueden romper seguridad, modos como mantenimiento, etc.).
  3. Usa env_file: para defaults en bloque pero mantenlo pequeño y predecible. Evita archivos “todo incluido” compartidos por cada servicio.
  4. Usa un único archivo env específico por entorno y por servicio si debes usar env files. El apilamiento está bien, pero requiere disciplina y validación.
  5. No publiques defaults significativos en la imagen a menos que lo decidas. Los defaults de imagen son invisibles para los lectores de Compose y causan sorpresas.
  6. Haz que las variables faltantes sean fatales. Una cadena vacía rara vez es un valor seguro para credenciales, endpoints o toggles.
  7. Recrea contenedores cuando cambien las env. Incorpora eso en tu procedimiento de despliegue; no confíes en la memoria humana.
  8. Registra la config Compose renderizada por cada despliegue para poder diffear lo que pretendías vs lo que enviaste.
  9. Mantén secretos fuera de las env vars. Usa archivos/secretos, pasa rutas y audita lo que tu diagnóstico vuelca.
  10. Añade una línea de log de arranque o un endpoint que imprima la config no secreta (sanitizada) para confirmar la interpretación de la app.

Paso a paso: cuando necesitas cambiar un valor de forma segura

  1. Cambia la capa autoritativa (normalmente environment: o un único archivo env final).
  2. Renderiza el resultado: ejecuta docker compose config y confirma que el valor aparece donde esperas.
  3. Envía el cambio: docker compose up -d --force-recreate service.
  4. Verifica en runtime: docker inspect y /proc/1/environ.
  5. Verifica la interpretación de la app: lee logs de arranque o consulta un endpoint de estado/configuración.
  6. Escribe la regla de precedencia para ese ajuste (aunque sea en una frase) para que nadie repita el incidente.

Preguntas frecuentes

1) ¿El .env se convierte automáticamente en variables de entorno del contenedor?

No. .env se usa principalmente por Compose para la sustitución de variables en el archivo Compose. Para inyectar valores en el contenedor, usa env_file: o environment:.

2) ¿Qué gana: env_file o environment?

environment gana. Si ambos definen FOO, el valor en environment: es el que obtiene el contenedor.

3) ¿Qué gana: ENV de la imagen o environment de Compose?

environment de Compose gana. Los defaults de la imagen son la capa base; la configuración en tiempo de ejecución los sobrescribe.

4) ¿Por qué a veces ${VAR} se sustituye por una cadena vacía sin fallar?

Porque Compose trata las variables de interpolación faltantes como “no establecidas” y puede por defecto dejarlas en blanco, a menudo emitiendo una advertencia. Si no tratas las advertencias como fallos, acabas enviando un valor vacío.

5) Puse una env var y reinicié el contenedor. ¿Por qué no se aplicó?

Un reinicio no cambia la configuración del contenedor. Necesitas recrearlo para que Docker almacene la nueva env en la configuración del contenedor.

6) ¿Es suficiente docker exec env para saber qué está configurado?

Es útil, pero revisa el entorno del PID 1 (/proc/1/environ) si sospechas que scripts de entrypoint o supervisores cambian el entorno. También confirma con docker inspect para ver qué piensa Docker que es la configuración.

7) ¿Son seguras las variables de entorno para secretos?

Son convenientes, no seguras. Pueden filtrarse mediante listados de procesos, dumps de crash, endpoints de depuración y bundles de soporte. Prefiere secretos montados como archivos y pasa rutas via env vars si es necesario.

8) ¿Por qué distintas máquinas producen configs Compose renderizadas diferentes?

Porque la interpolación depende del entorno donde se ejecuta Compose: variables de shell, un .env local y a veces distintos directorios de trabajo o wrappers. La config renderizada es un artefacto de build: trátalo como tal.

9) Si mi app lee DATABASE_URL y también DB_HOST, ¿qué debo hacer?

Elige un contrato y hazlo cumplir. Si debes soportar ambos, define una precedencia estricta en la app y registra cuál origen ganó al inicio (sin imprimir secretos).

10) ¿Cómo evito que el entorno del shell de los desarrolladores afecte despliegues en producción?

Ejecuta despliegues desde CI o un entorno de despliegue dedicado con un entorno saneado. Evita la interpolación ${VAR} para ajustes críticos en tiempo de ejecución a menos que las entradas estén controladas y validadas.

Conclusión: próximos pasos que detienen la hemorragia

Si has estado tratando las env vars como “simples”, Docker ha estado en desacuerdo silenciosamente contigo. El sistema no es malicioso: sólo está en capas. La cura es hacer esas capas explícitas y observables.

  1. Empieza a usar docker compose config como artefacto de despliegue de primera clase. Si no está renderizado, no es real.
  2. Haz rutinaria la verificación en tiempo de ejecución: docker inspect para config del contenedor, /proc/1/environ para la verdad del proceso.
  3. Colapsa capas innecesarias: menos env files, menos overrides, menos “defaults mágicos” en imágenes.
  4. Falla rápido ante variables faltantes. Las advertencias sobre defaults en blanco deben tratarse como un build roto.
  5. Saca los secretos de las env vars. Tu informe de incidentes futuro te lo agradecerá.

Cuando un sistema se comporta de forma extraña, el camino más rápido rara vez es intuición más profunda. Es preguntarle al runtime qué hizo y luego hacer difícil que los humanos lo hagan mal por accidente de nuevo.

← Anterior
Códigos de pitidos BIOS: diagnosticar fallos de hardware escuchando (y entrando en pánico)
Siguiente →
Ubuntu 24.04: límites de subida de PHP — corrige upload_max_filesize donde realmente importa (caso #10)

Deja un comentario