Pasar una contraseña de base de datos a un contenedor con environment: funciona, pero cualquiera que ejecute docker inspect la verá en texto plano. Esa es la trampa que separa la configuración normal de los datos sensibles. En esta guía verás las tres maneras de inyectar configuración con Docker Compose (la clave environment, el atributo env_file y el archivo .env), cómo se ordena su precedencia y por qué las contraseñas y las claves de API deben ir por el mecanismo de secretos, no por variables de entorno. La misma guía está disponible en inglés.

Puntos clave

  • Docker Compose resuelve las variables con una precedencia de cinco niveles: gana docker compose run -e, luego el shell interpolado, después environment, luego env_file y por último el ENV de la imagen.
  • El archivo .env sirve para interpolar valores dentro del docker-compose.yml (${VAR}), no para inyectarlos automáticamente en el contenedor: eso sigue siendo trabajo de environment o env_file.
  • Cuando la misma clave aparece en environment y en env_file, gana environment.
  • Los secretos en Docker Compose se definen como ficheros y se montan de solo lectura en /run/secrets/<nombre>, fuera del entorno del proceso y de la salida de docker inspect.
  • Las imágenes oficiales (PostgreSQL, MariaDB) admiten la convención _FILE: POSTGRES_PASSWORD_FILE=/run/secrets/db_password lee la contraseña del fichero en vez de una variable.
  • Desde Compose v2.24.0 puedes marcar un env_file como opcional con required: false.

¿Cómo se pasa la configuración a un contenedor?

Un contenedor arranca sin saber nada de tu servidor: no conoce el puerto que quieres exponer, ni el nombre de la base de datos, ni la zona horaria. Docker Compose te da tres vías para decírselo, y conviene no mezclarlas por accidente.

La primera es la clave environment dentro del servicio, donde escribes pares clave-valor directamente en el docker-compose.yml. La segunda es env_file, que apunta a uno o varios ficheros con esos mismos pares, uno por línea, para no ensuciar el Compose. La tercera es el archivo .env del directorio del proyecto, y aquí está la confusión más habitual: ese .env no se inyecta en el contenedor, sino que Compose lo usa para interpolar valores dentro del propio docker-compose.yml. Si defines APP_PORT=8080 en .env, puedes escribir "${APP_PORT}:8080" en la sección ports, pero el contenedor solo recibirá APP_PORT si además lo declaras en environment.

Esta distinción es la que más problemas causa al empezar. Antes de seguir, si aún no tienes el motor instalado, repasa cómo instalar Docker en Ubuntu 24.04.

¿Qué hace el archivo .env y la interpolación de variables?

Compose carga de forma automática un fichero llamado .env del directorio donde ejecutas el comando. Sus valores quedan disponibles para la interpolación: cualquier ${VARIABLE} o $VARIABLE que aparezca en el docker-compose.yml se sustituye por el valor correspondiente antes de crear los contenedores. Admite tanto la forma con llaves ${VAR} como la forma corta $VAR, y también valores por defecto con ${VAR:-valor}.

Un .env típico para el ejemplo de más abajo se ve así:

APP_PORT=8080
LOG_LEVEL=info
POSTGRES_VERSION=16.4

Con eso ya puedes referenciar ${APP_PORT} o ${POSTGRES_VERSION} en el Compose y cambiar el puerto o la versión de la imagen sin tocar el YAML. Lo importante: usa .env para parametrizar el docker-compose.yml, no como almacén de contraseñas, porque es un fichero en texto plano que acaba fácilmente en el repositorio. Añádelo siempre a tu .gitignore.

env_file frente a environment: ¿cuál gana?

Las dos claves inyectan variables dentro del contenedor, pero se comportan de forma distinta. environment se escribe en el Compose y es ideal para valores no sensibles y poco cambiantes. env_file externaliza esos pares a un fichero aparte, útil cuando tienes muchas variables o quieres un fichero por entorno (desarrollo, pruebas, producción).

Cuando una misma clave está en ambos sitios, gana environment. Y la precedencia global, de mayor a menor, sigue estas cinco reglas según la documentación de Docker:

  1. Lo que pasas con docker compose run -e en la línea de comandos.
  2. Un valor de environment o env_file interpolado desde el shell o desde un fichero de entorno.
  3. El valor puesto solo en environment.
  4. El valor puesto en env_file.
  5. El ENV definido en la imagen (el Dockerfile).

Desde Compose v2.24.0 puedes declarar un env_file opcional con required: false: si el fichero no existe, Compose ignora la entrada en silencio en vez de fallar. Y desde v2.30.0 el atributo format permite formatos alternativos de fichero. Estas mejoras conviven con el sello de Compose v2: ya no se escribe la clave version: al principio del fichero.

Secretos en Docker Compose: por qué no usar variables para contraseñas

Aquí está el cambio de mentalidad. Una variable de entorno es visible para cualquier proceso del contenedor, aparece en docker inspect, se hereda a los procesos hijos y suele acabar impresa en logs de depuración. La propia documentación de Docker lo dice sin rodeos: «Si inyectas contraseñas y claves de API como variables de entorno, te arriesgas a una exposición involuntaria de información». Su recomendación es clara: usa el mecanismo de secretos.

Un secreto en Compose es, en su forma más sencilla, un fichero cuyo contenido se monta de solo lectura en /run/secrets/<nombre> dentro del contenedor. No está en el entorno del proceso, no lo lista docker inspect y controlas su acceso con permisos de fichero normales. Las imágenes oficiales cierran el círculo con la convención _FILE: en lugar de POSTGRES_PASSWORD pasas POSTGRES_PASSWORD_FILE=/run/secrets/db_password, y la imagen lee la contraseña del fichero.

Este es un docker-compose.yml completo, listo para copiar, con una base de datos y una aplicación que comparten una contraseña como secreto:

services:
  db:
    image: postgres:16.4
    restart: unless-stopped
    environment:
      POSTGRES_DB: miapp
      POSTGRES_USER: miapp
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U miapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: nginx:1.27.2
    restart: unless-stopped
    env_file:
      - path: .env
        required: true
    environment:
      DB_HOST: db
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "${APP_PORT}:80"

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  db_data:

Antes de levantarlo, crea el fichero del secreto y el .env, y arranca la pila:

mkdir -p secrets
openssl rand -base64 24 > secrets/db_password.txt
chmod 600 secrets/db_password.txt

printf 'APP_PORT=8080\n' > .env

docker compose up -d
docker compose exec db cat /run/secrets/db_password

La última orden confirma que el secreto está montado dentro del contenedor. Si comparas la salida de docker inspect db con la de un servicio que usara POSTGRES_PASSWORD en environment, verás la diferencia: con el secreto, la contraseña no aparece por ningún lado. Para persistir bien esos datos, revisa cómo funcionan los volúmenes y bind mounts en Docker, y para que la app espere a una base de datos sana, apóyate en healthchecks y políticas de reinicio.

Un matiz importante: los secretos de Compose fuera de Swarm son bind mounts de ficheros locales. Docker Swarm añade secretos externos cifrados en su registro Raft, que creas con docker secret create y referencias con external: true. Para un homelab con un solo host, la versión de fichero es suficiente y mucho más sencilla.

Buenas prácticas y errores comunes

Después de montar unos cuantos servicios, estos son los tropiezos que más se repiten y cómo evitarlos:

  • No fijes :latest en producción. El tag :latest no es reproducible: una actualización silenciosa puede romperte el arranque. Fija una versión concreta como postgres:16.4 y usa :latest solo como aviso de lo que no debes hacer.
  • Ignora los ficheros sensibles. Añade .env, secrets/ y cualquier *.txt de contraseñas a tu .gitignore antes del primer commit. Un secreto en el historial de Git ya está comprometido.
  • No confundas .env con las variables del contenedor. El .env interpola en el YAML; para que una variable llegue al contenedor, decláralas en environment o env_file.
  • Da permisos estrictos al fichero del secreto. Un chmod 600 evita que otros usuarios del host lo lean.
  • Rota los secretos. Si vas a gestionar muchas credenciales, un gestor como Vaultwarden te ayuda a generarlas y guardarlas fuera del repositorio.

La idea de guardar la configuración en el entorno, y no en el código, es un principio clásico (lo formaliza la metodología Twelve-Factor App). Los secretos son la excepción práctica a ese principio: van en ficheros montados, no en variables.

Preguntas frecuentes

¿El archivo .env se inyecta automáticamente en el contenedor?

No. Compose carga .env para interpolar ${VARIABLE} dentro del docker-compose.yml, pero el contenedor no recibe esas variables a menos que las declares en environment o en env_file. Es la confusión más habitual: .env parametriza el Compose, no llena el entorno del contenedor por sí solo.

¿Puedo ver un secreto de Docker con docker inspect?

No, y ese es justo su propósito. A diferencia de una variable de environment, que docker inspect muestra en texto plano, un secreto se monta como fichero de solo lectura en /run/secrets/<nombre> y no forma parte de la configuración que expone la API de Docker.

¿Necesito Docker Swarm para usar secretos?

No para la versión basada en ficheros. Docker Compose monta secretos desde ficheros locales sin Swarm. Swarm solo es necesario para los secretos externos cifrados en el registro Raft, pensados para clústeres con varios nodos.

Conclusión

La regla es sencilla: configuración normal por environment o env_file, parametrización del Compose con .env, y contraseñas y claves siempre como secretos montados en /run/secrets/. Con esa separación tu docker-compose.yml queda limpio, reproducible y sin credenciales a la vista. El siguiente paso natural es encadenar servicios con dependencias sanas: repasa los healthchecks y las políticas de reinicio en Docker Compose para que la aplicación no arranque antes que su base de datos.

Fuentes: [1] Docker Docs: Environment variables in Compose[1], [2] Docker Docs: Manage secrets securely in Docker Compose[2], [3] docker/compose, Releases[3], [4] The Twelve-Factor App: Store config in the environment[4].

Fuentes

  1. Docker Docs: Environment variables in Compose
  2. Docker Docs: Manage secrets securely in Docker Compose
  3. docker/compose, Releases
  4. The Twelve-Factor App: Store config in the environment

Ruta: Self-hosting con Docker: de cero a producción