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 environment de la directiva env_file.
  • Usar archivos .env y la sustitución de variables ${VAR} dentro del docker-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.yml describa 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:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app_user
      POSTGRES_DB: app_db

También puede escribirse como lista usando el formato CLAVE=valor:

services:
  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=app_user
      - POSTGRES_DB=app_db

Explicación:

  • environment: bloque que enumera las variables que se inyectarán dentro del contenedor.
  • POSTGRES_USER / POSTGRES_DB: nombres de variable que la imagen postgres reconoce 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:

# db.env
POSTGRES_USER=app_user
POSTGRES_DB=app_db
POSTGRES_PASSWORD=changeme_in_real_env

Y lo referenciamos en el servicio:

services:
  db:
    image: postgres:16
    env_file:
      - db.env

Explicación:

  • env_file: lista de archivos cuyas variables se cargarán dentro del contenedor.
  • Cada línea del archivo .env sigue el formato CLAVE=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.env del 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:

  1. El archivo .env especial 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 propio docker-compose.yml.
  2. 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:

# .env
DB_USER=app_user
DB_NAME=app_db
DB_PASSWORD=changeme_in_real_env
WEB_PORT=8080

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, encuentra WEB_PORT=8080 y reemplaza ${WEB_PORT} por 8080. 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 mediante environment.
  • El archivo docker-compose.yml ya no contiene ningún valor concreto: es una plantilla reutilizable. Cambiando solo el .env adaptas la aplicación a otro entorno.

Puedes comprobar cómo queda el archivo ya con las sustituciones aplicadas, sin arrancar nada, con:

docker compose config

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 environment y con env_file, environment tiene prioridad sobre env_file.

Ejemplo práctico del primer plano. Con este .env:

# .env
WEB_PORT=8080

Si ejecutas:

WEB_PORT=9090 docker compose up

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 defines WEB_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 *.env con secretos) a tu .gitignore:
# .gitignore
.env
*.env
  • Versiona una plantilla, no los valores reales. Es habitual incluir en el repositorio un \.env.example con las claves pero sin valores sensibles, para documentar qué variables hace falta definir:
# .env.example
DB_USER=
DB_NAME=
DB_PASSWORD=
WEB_PORT=
  • Evita escribir secretos en la sección environment del YAML, ya que esta sí se sube normalmente al repositorio. Prefiere .env o env_file (excluidos de Git).
  • Recuerda que las variables de entorno son visibles para cualquiera que pueda ejecutar docker inspect sobre 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

myapp/
├── docker-compose.yml
├── .env
├── .env.example
└── .gitignore

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=16

Advertencia: changeme_in_real_env es 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 defines DB_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=
# .gitignore
.env

Paso 5: Verificar y Ejecutar

Primero comprueba que las sustituciones son correctas:

docker compose config

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:

docker compose up -d

Para probar que funciona la sobreescritura por shell sin tocar el .env, levanta la web en otro puerto:

WEB_PORT=9090 docker compose up -d

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 .env con env_file: el .env de la raíz sirve para sustituir ${VAR} en el YAML; env_file carga variables dentro del contenedor. No son intercambiables.
  • Esperar que env_file sustituya ${VAR} en el YAML: no lo hace. Solo el .env de la raíz (o --env-file) alimenta la sustitución del YAML.
  • Poner comillas innecesarias en el .env: en líneas como DB_USER=app_user no hace falta entrecomillar; en muchos casos las comillas pasan a formar parte literal del valor.
  • Dejar espacios alrededor del =: escribe DB_USER=app_user, no DB_USER = app_user. Los espacios pueden romper el análisis del archivo.
  • Olvidar el .gitignore: el error más peligroso. Comprueba que el .env con 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 config antes 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:

# .env
APP_PORT=8080

Archivo docker-compose.yml:

services:
  web:
    image: nginx:latest
    ports:
      - "${APP_PORT:-80}:80"

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:

# .env  (NO subir a Git)
DB_PASSWORD=changeme_in_real_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.

© Copyright 2026. Todos los derechos reservados