En esta lección aprenderemos a parametrizar nuestras aplicaciones de Docker Compose mediante variables de entorno. Hasta ahora hemos escrito valores fijos directamente dentro del archivo docker-compose.yml (puertos, contraseñas, nombres de base de datos, etc.). Ese enfoque funciona para practicar, pero en proyectos reales tiene problemas: mezcla configuración con código, dificulta reutilizar el mismo archivo en distintos entornos y expone secretos. Las variables de entorno resuelven todo esto separando la configuración de la definición de los servicios, lo que hace tu aplicación más segura, flexible y portable.
Objetivos de Aprendizaje
- Comprender por qué conviene usar variables de entorno en lugar de valores fijos.
- Diferenciar la directiva
environmentde la directivaenv_file. - Usar archivos
.envy la sustitución de variables${VAR}dentro deldocker-compose.yml. - Entender el orden de precedencia con el que Docker Compose resuelve cada variable.
- Aplicar valores por defecto con la sintaxis
${VAR:-default}. - Conocer las buenas prácticas de seguridad para el manejo de secretos.
¿Por Qué Usar Variables de Entorno?
Imagina que tienes una aplicación con una base de datos. Si escribes la contraseña y el nombre de la base de datos directamente en el docker-compose.yml, te encuentras con varios inconvenientes:
- Reutilización entre entornos: el mismo archivo debería servir para desarrollo, pruebas y producción, pero cada entorno necesita valores distintos (otra contraseña, otro puerto, otra URL).
- Seguridad: los secretos quedan escritos en un archivo que normalmente se sube al repositorio de código, donde cualquiera con acceso puede leerlos.
- Mantenimiento: si una contraseña aparece repetida en varios sitios, cambiarla obliga a buscar y editar todas las apariciones.
- Claridad: separar la configuración del código hace que el
docker-compose.ymldescriba la estructura de la aplicación, mientras que la configuración concreta vive aparte.
Las variables de entorno permiten escribir el docker-compose.yml una sola vez y suministrar valores diferentes según el contexto, sin tocar el archivo.
La Directiva environment vs env_file
Docker Compose ofrece dos formas principales de pasar variables de entorno al interior de un contenedor. Es importante no confundirlas.
| Característica | environment |
env_file |
|---|---|---|
| Dónde se definen los valores | Dentro del docker-compose.yml |
En un archivo externo (p. ej. db.env) |
| Uso recomendado | Pocas variables o valores derivados | Muchas variables o configuración separada |
| Visibilidad en el YAML | Visible directamente | No aparece en el YAML |
| Ideal para secretos | No (queda en el YAML) | Mejor (archivo aparte, fuera de Git) |
| Sintaxis | Lista o mapa de clave-valor | Ruta a uno o varios ficheros |
La Directiva environment
Define variables directamente dentro del servicio. Admite dos sintaxis equivalentes:
También puede escribirse como lista usando el formato CLAVE=valor:
Explicación:
environment: bloque que enumera las variables que se inyectarán dentro del contenedor.POSTGRES_USER/POSTGRES_DB: nombres de variable que la imagenpostgresreconoce para configurarse al arrancar.- Ambas sintaxis producen el mismo resultado; la de mapa (
clave: valor) suele ser más legible.
La Directiva env_file
En lugar de listar las variables en el YAML, las colocamos en un archivo aparte y le indicamos a Compose dónde encontrarlo. Primero creamos el archivo, por ejemplo db.env:
Y lo referenciamos en el servicio:
Explicación:
env_file: lista de archivos cuyas variables se cargarán dentro del contenedor.- Cada línea del archivo
.envsigue el formatoCLAVE=valor, sin comillas obligatorias y sin espacios alrededor del=. - Las líneas que empiezan por
#son comentarios y se ignoran.
Advertencia de seguridad: el archivo
db.envdel ejemplo contiene una contraseña en texto plano solo con fines didácticos. Nunca guardes secretos reales en texto plano ni los subas a tu repositorio. Más adelante veremos cómo gestionarlos correctamente.
El Archivo .env y la Sustitución de Variables ${VAR}
Aquí aparece un concepto que suele confundir a los principiantes, así que hay que distinguir dos usos distintos de los archivos con extensión .env:
- El archivo
.envespecial en la raíz del proyecto: Docker Compose lo lee automáticamente y sus valores sirven para sustituir las referencias${VAR}que escribas dentro del propiodocker-compose.yml. - Los archivos pasados con
env_file: cargan variables dentro del contenedor, pero no se usan para sustituir${VAR}en el YAML.
Vamos a centrarnos en el primero. Creamos un archivo llamado exactamente .env en la misma carpeta que el docker-compose.yml:
Ahora podemos referenciar esos valores en el docker-compose.yml con la sintaxis ${VAR}:
services:
web:
image: nginx:latest
ports:
- "${WEB_PORT}:80"
db:
image: postgres:16
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_NAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}Explicación detallada:
${WEB_PORT}: antes de arrancar, Compose lee el.env, encuentraWEB_PORT=8080y reemplaza${WEB_PORT}por8080. El mapeo final queda como"8080:80".${DB_USER},${DB_NAME},${DB_PASSWORD}: se sustituyen del mismo modo y, además, se inyectan dentro del contenedor de Postgres medianteenvironment.- El archivo
docker-compose.ymlya no contiene ningún valor concreto: es una plantilla reutilizable. Cambiando solo el.envadaptas la aplicación a otro entorno.
Puedes comprobar cómo queda el archivo ya con las sustituciones aplicadas, sin arrancar nada, con:
Este comando imprime el docker-compose.yml "resuelto", con todas las variables ya reemplazadas. Es muy útil para depurar.
Precedencia: Orden de Resolución de Variables
Cuando la misma variable está definida en varios sitios, Docker Compose necesita decidir cuál gana. Conocer este orden evita sorpresas. De mayor a menor prioridad (lo de arriba gana):
| Prioridad | Origen | Ejemplo |
|---|---|---|
| 1 (gana) | Variable del shell donde ejecutas Compose | WEB_PORT=9090 docker compose up |
| 2 | Variable pasada con --env-file |
docker compose --env-file prod.env up |
| 3 | Archivo .env de la raíz del proyecto |
WEB_PORT=8080 en .env |
| 4 (pierde) | Valor por defecto en el YAML | ${WEB_PORT:-80} |
Hay que separar dos planos distintos para no liarse:
- Sustitución en el YAML (
${VAR}): se resuelve con el orden de la tabla anterior (shell,--env-file,.env, valor por defecto). - Variables dentro del contenedor: si una variable se define a la vez con
environmenty conenv_file,environmenttiene prioridad sobreenv_file.
Ejemplo práctico del primer plano. Con este .env:
Si ejecutas:
El puerto efectivo será 9090, porque la variable del shell tiene más prioridad que el .env. Esto resulta cómodo para sobrescribir un valor puntualmente sin editar ningún archivo.
Valores por Defecto con ${VAR:-default}
¿Qué ocurre si una variable referenciada con ${VAR} no está definida en ningún sitio? Compose la sustituye por una cadena vacía y muestra una advertencia, lo que puede provocar errores difíciles de diagnosticar (por ejemplo, un mapeo de puerto ":80" inválido). Para evitarlo se usan valores por defecto.
| Sintaxis | Significado |
|---|---|
${VAR} |
Usa el valor de VAR; si no existe, cadena vacía. |
${VAR:-default} |
Si VAR no está definida o está vacía, usa default. |
${VAR-default} |
Si VAR no está definida (pero sí si está vacía), usa default. |
${VAR:?mensaje} |
Si VAR no está definida o vacía, detiene Compose mostrando mensaje. |
${VAR:+valor} |
Si VAR está definida y no vacía, usa valor; si no, cadena vacía. |
Ejemplo combinando varias de estas formas:
services:
web:
image: nginx:latest
ports:
- "${WEB_PORT:-80}:80"
db:
image: postgres:16
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_DB: ${DB_NAME:-app_db}
POSTGRES_PASSWORD: ${DB_PASSWORD:?Debes definir DB_PASSWORD en el archivo .env}Explicación:
${WEB_PORT:-80}: si no definesWEB_PORT, la aplicación arranca en el puerto 80 sin fallar. Aporta un valor sensato por defecto.${DB_USER:-postgres}y${DB_NAME:-app_db}: garantizan que el contenedor siempre reciba un usuario y una base de datos, aunque olvides configurarlos.${DB_PASSWORD:?...}: aquí no queremos un valor por defecto, porque una contraseña por defecto sería peligrosa. Si falta, Compose se detiene y muestra el mensaje, obligándote a definirla explícitamente. Es la opción recomendada para secretos.
Buenas Prácticas de Seguridad con Secretos
Las variables de entorno son cómodas, pero los secretos (contraseñas, claves de API, tokens) requieren cuidado adicional:
- Nunca subas secretos al repositorio. Añade el archivo
.env(y cualquier*.envcon secretos) a tu.gitignore:
- Versiona una plantilla, no los valores reales. Es habitual incluir en el repositorio un
\.env.examplecon las claves pero sin valores sensibles, para documentar qué variables hace falta definir:
- Evita escribir secretos en la sección
environmentdel YAML, ya que esta sí se sube normalmente al repositorio. Prefiere.envoenv_file(excluidos de Git). - Recuerda que las variables de entorno son visibles para cualquiera que pueda ejecutar
docker inspectsobre el contenedor o leer la lista de procesos del host. No son un mecanismo de cifrado. - Para secretos sensibles en producción, usa una solución dedicada: los Docker Secrets (disponibles en Docker Swarm), o un gestor externo como HashiCorp Vault o el gestor de secretos de tu proveedor de nube. Estas herramientas almacenan los valores de forma segura y los exponen solo al servicio que los necesita.
Resumen de seguridad: las variables de entorno separan la configuración del código y evitan tener secretos en el YAML, pero no cifran nada. Trátalas como información que cualquiera con acceso al host puede leer.
Ejemplo Completo: App Web + Base de Datos Parametrizada con .env
Vamos a montar una aplicación completa donde todo lo configurable se controla desde un único archivo .env. Esto demuestra el valor real de las variables de entorno.
Paso 1: Estructura del Proyecto
Paso 2: El Archivo .env
# .env (NO subir a Git: contiene secretos)
# --- Configuración web ---
WEB_PORT=8080
# --- Configuración base de datos ---
DB_USER=app_user
DB_NAME=app_db
DB_PASSWORD=changeme_in_real_env
DB_PORT=5432
# --- Versión de imágenes ---
POSTGRES_VERSION=16Advertencia:
changeme_in_real_enves un valor de ejemplo. En un entorno real usa una contraseña robusta y gestiónala con un gestor de secretos. Nunca dejes secretos en texto plano en producción.
Paso 3: El Archivo docker-compose.yml
services:
web:
image: nginx:latest
ports:
- "${WEB_PORT:-80}:80"
depends_on:
- db
db:
image: postgres:${POSTGRES_VERSION:-16}
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_DB: ${DB_NAME:-app_db}
POSTGRES_PASSWORD: ${DB_PASSWORD:?Debes definir DB_PASSWORD en el .env}
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:Explicación detallada:
"${WEB_PORT:-80}:80": el puerto del host se toma del.env(8080); si faltara, usaría 80. El puerto interno del contenedor (80) es fijo porque lo determina Nginx.postgres:${POSTGRES_VERSION:-16}: incluso la versión de la imagen es parametrizable. Cambiar de versión solo requiere editar el.env.POSTGRES_USER/POSTGRES_DB: tienen valores por defecto razonables, así que la app arranca aunque olvides configurarlos.POSTGRES_PASSWORD: ${DB_PASSWORD:?...}: obligatorio. Si no definesDB_PASSWORD, Compose se niega a arrancar y te lo dice. Así nunca tendrás una base de datos con contraseña vacía por descuido.db_data: volumen con nombre que persiste los datos de Postgres aunque elimines el contenedor.
Paso 4: El Archivo .env.example y el .gitignore
# .env.example (este SÍ se sube a Git, como documentación)
WEB_PORT=
DB_USER=
DB_NAME=
DB_PASSWORD=
DB_PORT=
POSTGRES_VERSION=Paso 5: Verificar y Ejecutar
Primero comprueba que las sustituciones son correctas:
Revisa en la salida que ${WEB_PORT} aparece ya como 8080, que la versión de Postgres es la esperada, etc. Si todo está bien, arranca la aplicación:
Para probar que funciona la sobreescritura por shell sin tocar el .env, levanta la web en otro puerto:
La aplicación quedará accesible en http://localhost:9090, demostrando que la variable del shell tiene prioridad sobre el .env.
Errores Comunes y Consejos
- Confundir
.envconenv_file: el.envde la raíz sirve para sustituir${VAR}en el YAML;env_filecarga variables dentro del contenedor. No son intercambiables. - Esperar que
env_filesustituya${VAR}en el YAML: no lo hace. Solo el.envde la raíz (o--env-file) alimenta la sustitución del YAML. - Poner comillas innecesarias en el
.env: en líneas comoDB_USER=app_userno hace falta entrecomillar; en muchos casos las comillas pasan a formar parte literal del valor. - Dejar espacios alrededor del
=: escribeDB_USER=app_user, noDB_USER = app_user. Los espacios pueden romper el análisis del archivo. - Olvidar el
.gitignore: el error más peligroso. Comprueba que el.envcon secretos nunca llega al repositorio. - Variables no definidas que pasan desapercibidas: una
${VAR}sin valor se convierte en cadena vacía silenciosamente. Usa${VAR:?mensaje}para los valores obligatorios y descubrir el problema al instante. - Consejo: usa siempre
docker compose configantes de un despliegue importante para ver el resultado final con las variables ya resueltas.
Ejercicios
Ejercicio 1
Crea un docker-compose.yml para un servidor Nginx cuyo puerto de host se tome de una variable APP_PORT definida en un archivo .env, con valor por defecto 80 si la variable no existe.
Ejercicio 2
Parametriza un servicio de base de datos MySQL de modo que el usuario y el nombre de la base de datos tengan valores por defecto, pero la contraseña sea obligatoria (Compose debe detenerse si no se define).
Ejercicio 3
Tienes un .env con APP_PORT=8080. Explica qué puerto se usará al ejecutar APP_PORT=3000 docker compose up y por qué.
Soluciones
Solución al Ejercicio 1
Archivo .env:
Archivo docker-compose.yml:
La expresión ${APP_PORT:-80} toma el valor del .env (8080) y, si la variable no estuviera definida ni vacía, usaría 80.
Solución al Ejercicio 2
Archivo .env:
Advertencia: usa una contraseña real y un gestor de secretos en producción; nunca dejes secretos en texto plano.
Archivo docker-compose.yml:
services:
db:
image: mysql:8
environment:
MYSQL_USER: ${DB_USER:-app_user}
MYSQL_DATABASE: ${DB_NAME:-app_db}
MYSQL_PASSWORD: ${DB_PASSWORD:?Debes definir DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?Debes definir DB_ROOT_PASSWORD}MYSQL_USER y MYSQL_DATABASE tienen valor por defecto; MYSQL_PASSWORD y MYSQL_ROOT_PASSWORD usan la sintaxis ${VAR:?mensaje}, que obliga a definirlas o detiene el arranque.
Solución al Ejercicio 3
Se usará el puerto 3000. La variable definida en el shell (APP_PORT=3000) tiene mayor prioridad que la del archivo .env (APP_PORT=8080). Según el orden de precedencia, el shell gana sobre el .env, de modo que la sustitución de ${APP_PORT} resuelve a 3000.
Conclusión
En esta lección hemos aprendido a parametrizar nuestras aplicaciones de Docker Compose con variables de entorno: la diferencia entre environment y env_file, el papel del archivo .env de la raíz en la sustitución ${VAR} del YAML, el orden de precedencia con el que se resuelven las variables, el uso de valores por defecto con ${VAR:-default} y las expresiones de validación ${VAR:?mensaje}, además de las buenas prácticas de seguridad para no exponer secretos. Con estas técnicas, un mismo docker-compose.yml se convierte en una plantilla reutilizable y segura para desarrollo, pruebas y producción.
Con esto cerramos el Módulo 4 dedicado a Docker Compose. En el Módulo 5: Conceptos Avanzados de Docker daremos un paso más allá, empezando por la Profundización en Redes Docker, donde estudiaremos en detalle cómo se comunican los contenedores entre sí, los distintos tipos de redes y cómo aislar y conectar servicios de forma controlada.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Registro y Monitoreo en Docker
Módulo 6: Docker en Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
