El incidente empieza pequeño. Un contenedor arranca con NODE_ENV=development, o tu base de datos de repente acepta
conexiones con una contraseña por defecto. Nada “cambió” en la aplicación. El job de CI está en verde. Enviaste el mismo
archivo Compose que enviaste la semana pasada.
Lo que cambió fue la parte más frágil de tu despliegue: la cadena invisible de variables de entorno que atraviesa Docker Compose, tu shell y un pequeño archivo .env que nadie revisa porque “no es código”.
Es código. Simplemente no se linttea.
Un modelo mental que no te engañará
Docker Compose usa las variables de entorno de dos maneras diferentes, y la mayoría de fallos en producción ocurren cuando los equipos
las tratan como si fueran lo mismo:
1) Variables usadas por Compose en sí (tiempo de renderizado)
Estas variables existen para renderizar la configuración de Compose: cosas como ${IMAGE_TAG} dentro
de compose.yaml, COMPOSE_PROJECT_NAME o COMPOSE_PROFILES.
Compose las resuelve antes de arrancar los contenedores. Si Compose las resuelve mal, puede que los contenedores ni siquiera sean los
que crees que desplegaste.
2) Variables pasadas a los contenedores (tiempo de ejecución)
Estas variables forman parte del entorno del contenedor: lo que tu app lee vía getenv.
Provienen de environment:, env_file:, y a veces del shell del host mediante
paso implícito.
Las variables de renderizado influyen en el YAML final. Las variables en tiempo de ejecución influyen en el comportamiento del proceso dentro del contenedor.
Confundir estas dos es cómo terminas “arreglando” un bug del contenedor editando el perfil de shell del host, para luego descubrir que
systemd no lee tu perfil de shell.
Una verdad operativa: no puedes “simplemente revisar el archivo .env.” Tienes que comprobar qué renderizó realmente Compose y qué recibió realmente el contenedor.
Cita para tener en tu escritorio: idea parafraseada
— “La esperanza no es una estrategia.” (idea parafraseada atribuida a
Gordon Sullivan, frecuentemente citada en círculos de ingeniería y fiabilidad)
Broma #1: Las variables de entorno son como el chisme de oficina—todos juran que no lo empezaron, pero de algún modo está en cada habitación.
Hechos e historia que deberías conocer (para dejar de discutir con YAML)
- Hecho 1: El archivo
.envusado por Docker Compose no es automáticamente el mismo formato que un script de shell. Es un parser más simple “KEY=VALUE” con sus propias particularidades. - Hecho 2: Compose se desarrolló originalmente a partir de Fig (2014), y gran parte de su comportamiento con variables es conveniencia heredada más que una elegancia de diseño pura.
- Hecho 3: Compose v2 está implementado como un plugin del CLI de Docker, y el comportamiento puede variar sutilmente entre versiones porque ahora el motor está más cercano al ecosistema del CLI de Docker.
- Hecho 4: Compose usa variables de entorno tanto para el renderizado de la configuración como para el entorno de los contenedores; reglas de precedencia diferentes aplican para cada ruta.
- Hecho 5: La interpolación de variables sucede antes de la mayoría de validaciones. Una variable faltante puede convertirse silenciosamente en una cadena vacía y aún formar un valor YAML “válido”.
- Hecho 6:
env_filees entrada en tiempo de ejecución para contenedores; generalmente no influye en la interpolación de Compose a menos que explícitamente cargues variables en el shell o uses una cadena de herramientas que lo haga. - Hecho 7: El comando
docker compose configes lo más parecido a un suero de la verdad: muestra la configuración completamente renderizada que Compose ejecutará. - Hecho 8: El mismo proyecto en dos hosts puede renderizar de forma diferente porque Compose lee el entorno del host, el directorio actual y entradas opcionales
--env-file. - Hecho 9:
COMPOSE_PROJECT_NAMEafecta nombres de redes, nombres de volúmenes y nombres de contenedores. Un cambio de nombre de proyecto puede “huir” volúmenes antiguos y crear volúmenes nuevos.
Guion de diagnóstico rápido
Cuando producción está en llamas, no necesitas filosofía. Necesitas una secuencia que reduzca rápidamente el radio de daño.
Aquí está el orden que uso porque separa bugs de “tiempo de renderizado” de bugs de “tiempo de ejecución” en minutos.
Primero: confirma qué renderizó Compose
-
Ejecuta
docker compose confige inspecciona los valores interpolados (tags de imagen, puertos, rutas de volúmenes,
nombre del proyecto, perfiles). Si la config renderizada es errónea, no pierdas tiempo dentro de los contenedores. - Revisa cadenas vacías, valores tipo “null”, valores por defecto inesperados o definiciones duplicadas de servicios debido a perfiles.
Segundo: confirma qué recibió realmente el contenedor
-
Inspecciona el entorno del contenedor (
docker inspect) o imprime dentro del contenedor
(env). -
Compáralo con lo que crees haber establecido vía
env_fileyenvironment.
Tercero: confirma qué .env y qué entorno del host se usaron
-
Verifica el directorio de trabajo y el archivo env seleccionado. Si ejecutaste Compose desde el directorio equivocado, podrías
estar usando el.envincorrecto. -
Revisa CI/CD: ¿pasa
--env-file? ¿exporta variables? ¿systemd limpia el entorno?
Si el almacenamiento o la red se ven raros, sospecha del nombre de proyecto y nombres de volúmenes
Un COMPOSE_PROJECT_NAME cambiado o un cambio de nombre de directorio puede crear redes nuevas y volúmenes nuevos.
La app “perdió” sus datos porque está escribiendo en un volumen distinto con otro nombre.
Tareas prácticas: comandos, salidas y decisiones
Estas son las pruebas de campo. Cada una incluye: un comando, qué significa una salida típica y la decisión que tomas.
Ejecútalas en orden cuando no estés seguro de dónde se oculta la verdad.
Tarea 1: Verificar la versión de Compose (el comportamiento varía)
cr0x@server:~$ docker compose version
Docker Compose version v2.24.6
Significado: Estás en Compose v2.x. Bien—la mayoría de comportamientos y flags modernos aplican.
Si esto fuera v1, varios flags y comportamientos límite difieren.
Decisión: Captura esta versión en las notas del incidente; si el comportamiento difiere entre hosts, alinea versiones.
Tarea 2: Ver qué nombre de proyecto piensa Compose
cr0x@server:~$ docker compose ls
NAME STATUS CONFIG FILES
payments-prod running(6) /srv/payments/compose.yaml
Significado: El proyecto es payments-prod. Redes/volúmenes se prefijarán con eso.
Decisión: Si esperabas otro nombre de proyecto, detente: podrías estar operando sobre el proyecto equivocado.
Tarea 3: Renderizar la config totalmente interpolada (la “verdad”)
cr0x@server:~$ cd /srv/payments
cr0x@server:~$ docker compose config
services:
api:
environment:
DB_HOST: db
LOG_LEVEL: info
image: registry.local/payments-api:1.9.3
ports:
- mode: ingress
target: 8080
published: "8080"
protocol: tcp
db:
environment:
POSTGRES_DB: payments
image: postgres:15
volumes:
payments-prod_db-data: {}
networks:
default:
name: payments-prod_default
Significado: La interpolación ocurrió. Esto es lo que Compose ejecutará.
Decisión: Si el tag de imagen o el puerto están mal aquí, el bug está en la resolución de variables (no en el runtime del contenedor).
Tarea 4: Identificar qué archivo env se está usando
cr0x@server:~$ ls -la /srv/payments/.env
-rw------- 1 root root 412 Jan 2 09:11 /srv/payments/.env
Significado: Existe un .env local en el directorio del proyecto.
Decisión: Verifica que estés ejecutando Compose desde este directorio; de lo contrario no estarás leyendo este archivo.
Tarea 5: Detectar minas de espacio en blanco y comillas en .env
cr0x@server:~$ sed -n '1,120p' /srv/payments/.env
IMAGE_TAG=1.9.3
DB_PASSWORD=correct-horse-battery-staple
LOG_LEVEL=info
API_BASE_URL=https://payments.internal
BAD_SPACES =oops
QUOTED="literal quotes?"
Significado: BAD_SPACES =oops es sospechoso: muchos parsers tratan esa clave como BAD_SPACES (con un espacio final) o la rechazan.
QUOTED="literal quotes?" puede preservar las comillas dependiendo del parser.
Decisión: Arregla el formato: no espacios alrededor de =, evita comillas a menos que conozcas el comportamiento del parser.
Tarea 6: Comprobar si falta una variable en tiempo de renderizado
cr0x@server:~$ grep -n 'IMAGE_TAG' -n /srv/payments/compose.yaml
12: image: registry.local/payments-api:${IMAGE_TAG}
Significado: Compose necesita IMAGE_TAG para renderizar la cadena de imagen.
Decisión: Asegura que IMAGE_TAG esté establecido en el .env correcto o exportado en el entorno usado por Compose.
Tarea 7: Detectar interpolación vacía silenciosa
cr0x@server:~$ IMAGE_TAG= docker compose config | grep -n 'image:'
7: image: registry.local/payments-api:
Significado: Un IMAGE_TAG vacío renderiza una referencia de imagen medio inválida que aún podría pasar el parseo YAML.
Decisión: Añade comprobaciones de variables requeridas usando valores por defecto/interpolación de Compose (ver más adelante) y falla el CI en vacíos.
Tarea 8: Inspeccionar el entorno dentro de un contenedor en ejecución
cr0x@server:~$ docker compose exec -T api env | egrep 'DB_|LOG_LEVEL|API_BASE_URL'
API_BASE_URL=https://payments.internal
DB_HOST=db
LOG_LEVEL=info
Significado: El contenedor recibió variables. Si falta algo, es un problema de inyección de entorno en tiempo de ejecución.
Decisión: Compáralo con docker compose config y el contenido de env_file.
Tarea 9: Confirmar qué piensa Docker que es el entorno (autoritativo)
cr0x@server:~$ docker inspect payments-prod-api-1 --format '{{json .Config.Env}}'
["API_BASE_URL=https://payments.internal","DB_HOST=db","LOG_LEVEL=info","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
Significado: Esto es lo que Docker pasará al proceso. Si no está aquí, tu app no lo verá.
Decisión: Si la config de Compose lo muestra pero inspect no, tienes una deriva de despliegue o un problema de recreación.
Tarea 10: Detectar problemas de “no recreó el contenedor”
cr0x@server:~$ docker compose up -d
[+] Running 2/2
✔ Container payments-prod-db-1 Running
✔ Container payments-prod-api-1 Running
Significado: Compose no recreó contenedores. Los cambios de env no se aplicarán a un contenedor en ejecución a menos que se recree.
Decisión: Si cambiaste vars de entorno, fuerza la recreación: docker compose up -d --force-recreate.
Tarea 11: Forzar recrear y confirmar que se aplicó el nuevo entorno
cr0x@server:~$ docker compose up -d --force-recreate
[+] Running 2/2
✔ Container payments-prod-db-1 Running
✔ Container payments-prod-api-1 Started
Significado: El contenedor API fue reiniciado/recreado.
Decisión: Repite la Tarea 8/9 para confirmar que los cambios de entorno realmente llegaron.
Tarea 12: Detectar deriva de nombre de proyecto que crea volúmenes “nuevos”
cr0x@server:~$ docker volume ls | grep -E 'payments.*db-data'
local payments-prod_db-data
local payments_db-data
Significado: Existen dos volúmenes con nombres similares, probablemente de nombres de proyecto diferentes.
Decisión: Confirma qué volumen está adjunto al contenedor DB en ejecución antes de “limpiar” nada.
Tarea 13: Confirmar qué volumen está usando realmente un contenedor
cr0x@server:~$ docker inspect payments-prod-db-1 --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
payments-prod_db-data /var/lib/postgresql/data
Significado: La BD está usando payments-prod_db-data.
Decisión: Si la app “perdió datos”, compáralo con el volumen que esperabas. No borres volúmenes hasta que pruebes que no se usan.
Tarea 14: Identificar qué .env se usa cuando ejecutas desde otro directorio
cr0x@server:~$ cd /tmp
cr0x@server:~$ docker compose -f /srv/payments/compose.yaml config | head
services:
api:
image: registry.local/payments-api:
Significado: Ejecutar desde /tmp probablemente hizo que Compose no encontrara el /srv/payments/.env previsto, por lo que la interpolación falló.
Decisión: Siempre ejecuta desde el directorio del proyecto o suministra --env-file /srv/payments/.env.
Tarea 15: Probar precedencia entre host y .env
cr0x@server:~$ cd /srv/payments
cr0x@server:~$ export IMAGE_TAG=2.0.0
cr0x@server:~$ docker compose config | grep -n 'image:'
7: image: registry.local/payments-api:2.0.0
Significado: La variable exportada en el host sobreescribió el valor en .env.
Decisión: En producción, evita confiar en “lo que esté exportado en el shell”. Haz explícita la fuente de las variables.
Tarea 16: Detectar CRLF accidental de Windows en .env (sí, aún sucede)
cr0x@server:~$ file /srv/payments/.env
/srv/payments/.env: ASCII text, with CRLF line terminators
Significado: CRLF puede colarse en claves/valores, causando desconcertantes “variable no encontrada” o valores con \r ocultos.
Decisión: Convierte a LF: sed -i 's/\r$//' /srv/payments/.env, luego vuelve a renderizar la config.
Tarea 17: Confirmar retornos de carro ocultos en un valor específico
cr0x@server:~$ python3 -c 'import os;print(repr(open("/srv/payments/.env","rb").read().splitlines()[-1]))'
b'QUOTED="literal quotes?"\r'
Significado: Ese \r final es real. Puede romper tokens de autenticación, URLs o contraseñas.
Decisión: Normaliza finales de línea en CI y trata .env como un artefacto de texto que necesita comprobaciones.
Tarea 18: Mostrar diferencias entre env_file y environment en la config final
cr0x@server:~$ docker compose config | sed -n '1,80p'
services:
api:
environment:
DB_HOST: db
LOG_LEVEL: info
image: registry.local/payments-api:1.9.3
Significado: Puedes ver los valores inline de environment: claramente. Si usaste env_file, puede que no se expanda inline en la salida como esperas.
Decisión: Si tu auditoría depende de ver variables, no confíes solo en env_file; usa pasos explícitos de validación de configuración.
Precedencia y alcance: quién gana cuando las variables chocan
La mayoría de equipos no puede responder esta pregunta sin adivinar: “Si establezco FOO en el shell, en .env,
y en environment:, ¿cuál gana?” La respuesta depende de si te refieres a tiempo de renderizado o tiempo de ejecución.
Por eso esto sigue rompiéndose en producción: la gente habla sin comprenderse.
Precedencia en tiempo de renderizado (interpolación en compose.yaml)
Cuando Compose interpola ${VAR} en el YAML, mira sus fuentes de entorno. En la práctica, el entorno exportado del proceso Compose es el competidor fuerte. El .env local suele ser un comodín de respaldo.
En otras palabras: si tu CI exporta IMAGE_TAG, normalmente sobreescribirá el .env. Si tu unidad systemd ejecuta Compose con un entorno mayormente vacío, ignorará lo que tenía tu shell interactivo.
Regla operativa: las variables de renderizado deben ser explícitas. O pásalas vía CI de forma controlada o suministra un env file explícito con --env-file. No dejes que shells aleatorios decidan.
Precedencia en tiempo de ejecución (lo que recibe el contenedor)
El entorno del contenedor se construye a partir de las definiciones de servicio de Compose:
environment:entradas son explícitas y visibles en el archivo Compose.env_file:carga pares clave/valor desde un archivo al entorno del contenedor.- Algunas variables pueden pasarse desde el host si las referencias en
environment:sin valor, dependiendo de la sintaxis.
Regla práctica: trata environment: como una API de lo que el contenedor espera y
trata env_file como un detalle de implementación de cómo le das esos valores. Al depurar,
siempre comprueba qué llegó realmente en docker inspect.
El nombre de proyecto es parte secreta de la historia del entorno
COMPOSE_PROJECT_NAME parece “solo un nombre.” No lo es.
Cambia nombres de redes y volúmenes. Si atás tus datos a volúmenes y tu monitorización a nombres de contenedores,
el nombre del proyecto es una variable de producción, lo reconozcas o no.
Interpolación y parseo: los bordes afilados en .env y Compose
El formato .env parece shell. No es shell. Es un archivo key/value con la flexibilidad justa para que te sientas demasiado confiado.
Espacios en blanco: el asesino silencioso
Muchos parsers tratan KEY =value como una clave diferente a KEY=value, o la rechazan.
De cualquier modo, terminas con “KEY no establecido” y Compose sustituye silenciosamente una cadena vacía.
No “seas tolerante.” Sé estricto. Para archivos env en producción:
no espacios alrededor del igual, y las claves deberían coincidir con [A-Z0-9_]+.
Comillas: a veces literales, a veces eliminadas, siempre confusas
En algunos ecosistemas, FOO="bar" significa que el valor es bar sin comillas. En otros, esas
comillas forman parte del valor. El comportamiento de Compose puede sorprender dependiendo de qué ruta de parseo estés usando.
La única postura segura: evita comillas en .env a menos que hayas verificado el comportamiento con docker compose config y un contenedor en ejecución.
Defaults de interpolación: úsalos, pero entiéndelos
Compose soporta patrones como:
${VAR:-default} y ${VAR?error} en muchos contextos.
Aquí es donde los equipos pueden convertir un fallo invisible en un fallo ruidoso.
Si IMAGE_TAG debe existir en prod, hazlo requerido. Si LOG_LEVEL puede tener un valor por defecto, asígnalo.
Falla rápido en todo lo que cambie el comportamiento de formas que no puedas ver.
Vacío es distinto de no establecido
La interpolación de Compose a menudo trata una variable vacía como “establecida,” lo que puede derrotar los defaults. Si una pipeline
pone IMAGE_TAG como cadena vacía (sí, sucede), tu ${IMAGE_TAG:-latest} puede o no comportarse como esperas.
Prueba esto explícitamente en tu entorno.
.env vs env_file: mismo aspecto, distinta semántica
El archivo .env (para Compose) se usa para la interpolación de variables de Compose y algunas configuraciones de Compose.
La directiva env_file: alimenta el entorno de ejecución de los contenedores.
La gente los mezcla porque el contenido de los archivos parece idéntico. El resultado es de confianza caótica.
Si quieres que valores influyan en la interpolación, deben estar en el entorno que Compose usa para renderizar
(exportados en el shell, --env-file explícito, o el manejo de entorno de tu orquestador). Si quieres valores dentro del contenedor,
deben estar en environment: o env_file:.
Broma #2: Un archivo .env es como un niño pequeño—que esté callado no significa que esté bien, significa que deberías comprobarlo inmediatamente.
Tres micro-historias corporativas desde las trincheras
Historia 1: El incidente causado por una suposición errónea
Un equipo fintech ejecutaba una API orientada al cliente en un par de VMs con Docker Compose. Tenían un .env en el repo
y un prod.env separado almacenado en el host. En su cabeza, “Compose carga env desde env_file.” Estaban
medio en lo cierto y totalmente condenados.
El archivo Compose usaba ${IMAGE_TAG} para fijar la imagen de la API. Las variables de runtime del contenedor venían de
env_file: ./prod.env. Un candidato a release necesitaba un hotfix, así que un ingeniero actualizó IMAGE_TAG
en prod.env, ejecutó docker compose up -d y esperó que la nueva imagen se desplegara.
No ocurrió. La interpolación de Compose no miró env_file para renderizar el campo image:.
Los contenedores se quedaron en el tag antiguo. Mientras tanto, el ingeniero también actualizó una variable de runtime en prod.env
y asumió que el contenedor la tomó; no fue así, porque Compose no recreó el contenedor. Así que ahora tenían
código viejo, env viejo y una nueva creencia.
Dos horas después, la API lanzaba errores que parecían una regresión del hotfix. No lo era. El hotfix nunca se desplegó. Su monitorización mostraba “despliegue completado”
porque el job finalizó; no validó la salida de docker compose config ni comprobó los IDs de imagen de los contenedores en ejecución.
La solución fue aburrida: hacer que los tags de imagen sean una variable requerida en tiempo de renderizado y establecerla explícitamente en el comando de despliegue,
verificar con docker compose config, luego forzar la recreación o hacer roll de contenedores correctamente. También dejaron de
usar prod.env como un archivo mágico que “controla todo.” Controla exactamente lo que conectas a él.
Historia 2: La optimización que salió mal
Una compañía de medios quería despliegues más rápidos. Alguien notó que recrear contenedores toma tiempo, especialmente para un
servicio con muchos sidecars. Cambiaron el proceso: actualizar .env, luego ejecutar docker compose up -d
sin forzar recreación, para “evitar downtime.”
Por un tiempo, pareció funcionar—porque la mayoría de cambios eran cambios de tag de imagen, y Compose tiraría y reiniciaría
servicios cuando detectara una imagen nueva. Pero las variables de entorno no son imágenes. Un cambio crítico de configuración
alternó un flag de feature para el enrutamiento de peticiones. La mitad de la flota se actualizó (nodos nuevos donde los contenedores se recrearon),
la otra mitad no. El resultado fue comportamiento de cerebro dividido donde las peticiones tomaban rutas distintas según la VM.
El debugging fue doloroso porque el archivo Compose se veía correcto, el .env se veía correcto y los
contenedores estaban todos “arriba.” El bug estaba en el proceso: optimizaron la única acción que aplica reliably cambios de env.
Introdujeron no determinismo en el despliegue de configuración.
El playbook de recuperación fue contundente: si el env cambió, los contenedores se recrean. Si quieres cero downtime,
lo haces con balanceadores y reinicios en rolling, no esperando que Compose infiera tu intención.
Historia 3: La práctica correcta y aburrida que salvó el día
Un equipo SaaS B2B ejecutaba stacks basados en Compose para servicios internos: métricas, job runners y una base de datos legacy.
Eran alérgicos a lo “ingenioso.” Su despliegue en producción requería tres comprobaciones:
renderizar la config, validar los IDs de imagen en ejecución y registrar el checksum efectivo del entorno.
Un viernes, se fusionó un cambio que introdujo una nueva variable RATE_LIMIT_MODE usada en la interpolación de Compose
para seleccionar la imagen de un sidecar. El desarrollador la añadió a .env.example pero olvidó la fuente de env de producción.
La pipeline de CI tampoco la estaba exportando.
El job de despliegue falló temprano porque su Compose usó ${RATE_LIMIT_MODE?must be set}.
Ese es el truco completo: convirtieron la interpolación vacía silenciosa en una parada en seco. No hubo despliegue parcial, ni comportamiento misterioso.
Arreglaron la pipeline, desplegaron el lunes y nadie recibió paginación. Fue tan anodino que molestó al equipo.
Así sabes que fue correcto.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: el tag de imagen queda en blanco o “latest” inesperadamente
Causa raíz: Variable de render-time faltante o vacía, Compose interpola a cadena vacía; o CI exporta una variable vacía que sobreescribe .env.
Solución: Usa interpolación requerida: image: myapp:${IMAGE_TAG?set IMAGE_TAG}. En CI, falla si IMAGE_TAG está vacío. Valida con docker compose config.
2) Síntoma: “Actualicé .env pero el contenedor no cambió comportamiento”
Causa raíz: El contenedor no se recreó; el contenedor en ejecución mantiene el entorno antiguo.
Solución: Aplica cambios con docker compose up -d --force-recreate (o docker compose restart si procede, pero recrear es más seguro para cambios de env). Verifica con docker inspect ... Config.Env.
3) Síntoma: producción usa settings de desarrollo aunque exista prod.env
Causa raíz: Compose está leyendo .env desde el directorio de trabajo actual, no desde la ruta prevista; o --env-file no se suministra en la automatización.
Solución: En systemd/CI, ejecuta desde el directorio del proyecto o especifica --env-file /srv/app/.env. Añade una comprobación que imprima el checksum del env durante el despliegue.
4) Síntoma: falla la autenticación por contraseña, pero el valor “se ve bien”
Causa raíz: CRLF o espacios finales en .env inyectan caracteres ocultos (a menudo \r) en el valor.
Solución: Normaliza finales de línea (sed -i 's/\r$//'), y valida imprimiendo la repr o hexdump del valor en un contenedor de prueba controlado.
5) Síntoma: la base de datos “perdió” datos después de un redeploy
Causa raíz: Cambio de nombre de proyecto (cambio de nombre de directorio, cambio de COMPOSE_PROJECT_NAME), creando un volumen nuevo con nombre distinto.
Solución: Fija el nombre de proyecto explícitamente para producción. Audita con docker volume ls y docker inspect mounts antes de limpiar. Trata los nombres de volumen como parte del estado.
6) Síntoma: variables en el contenedor no coinciden con las del .env
Causa raíz: Confundir .env (render-time de Compose) con env_file (runtime del contenedor); o el entorno del host sobreescribe valores.
Solución: Decide qué fuente es autorizada. Para valores críticos en runtime, usa claves explícitas en environment: y súmelas desde un archivo env controlado. Para render-time, pásalas con --env-file y valida la salida de la config.
7) Síntoma: una variable con comillas se comporta de forma extraña
Causa raíz: Las comillas se tratan literal o se eliminan de forma diferente a la esperada; parsers distintos en la cadena de herramientas.
Solución: Elimina comillas en .env salvo que sean necesarias. Cuando sean necesarias, valida con docker compose config e inspecciona dentro del contenedor.
8) Síntoma: el servicio no arranca, el mapeo de puertos es absurdo
Causa raíz: La interpolación produjo una cadena de puerto inválida (vacía, no numérica, incluye espacio), pero YAML aún parsea.
Solución: Requiere variables y valida puertos en CI grepeando la config renderizada. Usa defaults solo para valores seguros de desarrollo.
9) Síntoma: “funciona localmente, falla en CI” con el mismo archivo Compose
Causa raíz: El shell local exporta variables y CI no; o CI tiene diferente locale/finales de línea; o CI ejecuta desde otro directorio.
Solución: Haz explícita la fuente de env en CI. Imprime docker compose config (o al menos las líneas relevantes) y asegúrate de que sea determinista.
10) Síntoma: secretos aparecen en logs o bundles de soporte
Causa raíz: Almacenar secretos en .env e imprimir la config renderizada o el env del contenedor durante la depuración; las vars de entorno se filtran fácilmente vía listados de procesos y volcados de crash.
Solución: Usa secretos de Compose cuando sea posible, o credenciales montadas en archivos con permisos estrictos. En la herramienta de incidentes, redacta salidas de env por defecto.
Listas de verificación / plan paso a paso para producción
Lista A: Hacer la interpolación de Compose determinista
- Fija la fuente de env: En la automatización, siempre ejecuta con una ruta
--env-fileexplícita y un directorio de trabajo fijo. - Requerir variables críticas: Usa
${VAR?message}para tags de imagen, endpoints externos y nombres de proyecto. - Deja de exportar variables aleatorias: Limpia el entorno en los jobs de CI. Si es necesario, establécelo explícitamente.
- Renderiza y diff: Guarda la salida de
docker compose configcomo artefacto de build y haz diff contra despliegues previos.
Lista B: Hacer el entorno runtime del contenedor auditable
- Documenta el contrato: Lista las vars de entorno de runtime requeridas por servicio (nombres, significado, valores permitidos).
- Prefiere claves explícitas en
environment:: Hace el contrato visible en code review. - Usa
env_filepara valores en bloque, no para comportamiento misterioso: Manténlo mínimo y estructurado. Evita mezclar “dev” y “prod” en el mismo archivo. - Recrear en cambios de env: Si el entorno runtime cambió, los contenedores deben recrearse. Planifica downtime/rolling en consecuencia.
Lista C: No dejes que el estado derive (volúmenes/redes)
- Fija el nombre del proyecto: Establece
name:en el modelo Compose oCOMPOSE_PROJECT_NAMEen una fuente de env controlada. - Declara volúmenes explícitamente: Usa volúmenes nombrados para servicios stateful; evita volúmenes anónimos accidentales.
- Audita antes de limpiar: Siempre inspecciona mounts y referencias de contenedores antes de eliminar volúmenes.
Lista D: Trata .env como código de producción
- Permisos:
chmod 600 .envsi contiene material sensible. - Normaliza finales de línea: Impone LF en CI.
- Reglas de lint: No espacios alrededor de
=, no tabs, no espacios finales, patrones de clave predecibles. - Control de cambios: Requiere revisión para cambios en env, y guarda un historial (aunque el archivo esté seguro fuera de Git).
Guía operativa que previene la mayoría de incidentes .env
Usa defaults solo para ergonomía de desarrollador, no para seguridad en producción
Defaults como ${LOG_LEVEL:-debug} están bien para trabajo local. En producción pueden convertir una config faltante
en comportamiento sorprendente. Prefiere valores explícitos en fuentes de env de producción y variables requeridas para todo lo que
afecte integridad de datos, autenticación o enrutamiento.
Falla temprano en el host, no tarde en el contenedor
Si una variable es requerida, falla en tiempo de renderizado. Quieres que el despliegue se detenga antes de tirar imágenes, antes de
tocar volúmenes, antes de reiniciar cualquier cosa. Es más barato y más seguro.
Deja de tratar secretos como “solo vars de entorno”
Las variables de entorno se filtran. Se filtran en reportes de crash, endpoints de debug, listados de procesos, bundles de soporte accidentales y capturas de pantalla humanas. También permanecen en metadata de contenedores más tiempo de lo que crees.
Usa mecanismos de secretos cuando puedas. Si no puedes, al menos separa secretos de no-secretos y diseña comandos de diagnóstico para redactar por defecto.
Haz la configuración observable
Tu sistema debería reportar la versión de configuración efectiva sin volcar secretos. Un checksum de config,
un SHA de git, un digest de imagen y una variable “mode” no sensible suelen ser suficientes para confirmar que el sistema es lo que crees.
Preguntas frecuentes
1) ¿Compose carga automáticamente .env?
Típicamente, sí—.env en el directorio del proyecto se usa como fuente cómoda para la interpolación de variables de Compose y ciertas configuraciones de Compose. Pero “directorio del proyecto” depende de dónde ejecutes el comando y cómo referencias el archivo Compose. Si ejecutas desde el directorio equivocado, puedes cargar silenciosamente el .env equivocado o ninguno.
2) ¿Es .env lo mismo que env_file?
No. .env comúnmente influye la interpolación en tiempo de renderizado de Compose. env_file inyecta
variables dentro del contenedor en tiempo de ejecución. Los archivos se ven similares; la semántica es distinta. Confundirlos es un modo clásico de fallo.
3) ¿Por qué no se aplicó mi cambio en .env después de docker compose up -d?
Porque los contenedores no absorben mágicamente nuevas variables de entorno. Si Compose no recrea el contenedor,
el entorno en ejecución permanece igual. Usa docker compose up -d --force-recreate cuando cambie el env,
y verifica vía docker inspect.
4) ¿Qué gana: las variables exportadas en el shell o .env?
En muchos setups comunes, las variables exportadas en el entorno que ejecuta Compose sobreescriben valores de .env.
Por eso ocurre “funciona en mi máquina”: tu shell exporta algo que CI no, o viceversa. Haz explícita la fuente de env en la automatización.
5) ¿Puedo tener múltiples archivos env?
Sí, pero sé intencional sobre el propósito: uno para render-time (pasado con --env-file) y posiblemente uno
o más para inyección runtime (env_file: por servicio). Evita apilar tantos archivos que nadie pueda predecir el resultado.
6) ¿Por qué mi app ve comillas en los valores?
Porque tu parser podría tratar las comillas como literales. El formato .env no es un estándar universal y
diferentes herramientas interpretan comillas y escapes de forma distinta. Si necesitas caracteres especiales, prueba la ruta exacta:
render-time vía docker compose config y runtime vía docker inspect.
7) ¿Cómo evito que variables vacías se cuelen en producción?
Usa interpolación requerida (${VAR?message}) para valores críticos y añade checks en CI que fallen si
la config renderizada contiene tags de imagen en blanco, puertos vacíos o hostnames vacíos. Esta es una de las correcciones de mayor impacto que puedes entregar.
8) ¿Por qué redeplegar creó volúmenes nuevos y “borró datos”?
Probablemente un cambio de nombre de proyecto. Compose prefija nombres de volúmenes y redes con el nombre del proyecto, que viene del
nombre del directorio, configuración explícita o entorno. Fíjalo para producción para que los volúmenes permanezcan estables. Luego confirma que
el contenedor DB está adjunto al volumen previsto antes de limpiar.
9) ¿Es seguro imprimir docker compose config en logs de CI?
No siempre. Si inlineas secretos en el archivo Compose o los interpolas en campos mostrados en la salida, puedes filtrar credenciales. Si debes imprimir la config, redácta las claves sensibles o imprime solo líneas específicas (referencias de imagen, puertos, settings no sensibles).
10) ¿Cuándo debo usar secretos de Compose en lugar de vars de entorno?
Usa secretos cuando puedas: credenciales, tokens de API, claves privadas, cualquier cosa que lamentarías ver en un log o dump de crash.
Las vars de entorno están bien para configuración no sensible y toggles de features. Si debes usar vars para secretos, restringe permisos y reduce dónde se muestran.
Próximos pasos que puedes hacer esta semana
-
Añade una “comprobación de renderizado” en CI: ejecuta
docker compose configy falla en campos críticos vacíos
(tags de imagen, puertos, hostnames). Guarda la config renderizada como artefacto con secretos redactados. -
Haz variables críticas requeridas: convierte
${VAR}en${VAR?set VAR}para
puntos de interpolación críticos en producción. - Fija el nombre de proyecto en producción: evita la deriva accidental de volúmenes y redes. Trátalo como estado.
-
Estandariza la ejecución de despliegue: directorio de trabajo fijo,
--env-fileexplícito,
y una política: cambios de env requieren recreación o reinicio rolling. -
Deja de almacenar secretos en archivos .env casuales: muévelos a un mecanismo de secretos o montajes de archivos y
ajusta las herramientas de diagnóstico para evitar filtrarlos durante incidentes.
Docker Compose está bien. Lo que no está bien son las suposiciones no declaradas alrededor de .env.
Haz las variables explícitas, la config renderizada observable y el entorno del contenedor verificable.
Entonces la próxima “regresión misteriosa” será un diff de cinco minutos en lugar de un fin de semana.