Variables de entorno y secretos en Docker Compose
Índice de contenidos
- Puntos clave
- ¿Cómo se pasa la configuración a un contenedor?
- ¿Qué hace el archivo .env y la interpolación de variables?
- env_file frente a environment: ¿cuál gana?
- Secretos en Docker Compose: por qué no usar variables para contraseñas
- Buenas prácticas y errores comunes
- Preguntas frecuentes
- ¿El archivo .env se inyecta automáticamente en el contenedor?
- ¿Puedo ver un secreto de Docker con docker inspect?
- ¿Necesito Docker Swarm para usar secretos?
- Conclusión
- Fuentes
Docker Compose ofrece tres formas de pasar configuración a un contenedor: la clave environment, el atributo env_file y el archivo .env para interpolar valores. Para datos sensibles no uses variables de entorno; los secretos en Docker Compose se montan como ficheros de solo lectura en /run/secrets/, lejos de los logs y del entorno del proceso.
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ésenvironment, luegoenv_filey por último elENVde la imagen. - El archivo
.envsirve para interpolar valores dentro deldocker-compose.yml(${VAR}), no para inyectarlos automáticamente en el contenedor: eso sigue siendo trabajo deenvironmentoenv_file. - Cuando la misma clave aparece en
environmenty enenv_file, ganaenvironment. - 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 dedocker inspect. - Las imágenes oficiales (PostgreSQL, MariaDB) admiten la convención
_FILE:POSTGRES_PASSWORD_FILE=/run/secrets/db_passwordlee la contraseña del fichero en vez de una variable. - Desde Compose v2.24.0 puedes marcar un
env_filecomo opcional conrequired: 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:
- Lo que pasas con
docker compose run -een la línea de comandos. - Un valor de
environmentoenv_fileinterpolado desde el shell o desde un fichero de entorno. - El valor puesto solo en
environment. - El valor puesto en
env_file. - El
ENVdefinido en la imagen (elDockerfile).
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
:latesten producción. El tag:latestno es reproducible: una actualización silenciosa puede romperte el arranque. Fija una versión concreta comopostgres:16.4y usa:latestsolo como aviso de lo que no debes hacer. - Ignora los ficheros sensibles. Añade
.env,secrets/y cualquier*.txtde contraseñas a tu.gitignoreantes del primer commit. Un secreto en el historial de Git ya está comprometido. - No confundas
.envcon las variables del contenedor. El.envinterpola en el YAML; para que una variable llegue al contenedor, decláralas enenvironmentoenv_file. - Da permisos estrictos al fichero del secreto. Un
chmod 600evita 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.