Cómo Usar Variables de Entorno en Docker: ARG vs ENV
Aprende cómo funcionan las variables de entorno en Docker: ARG para build time, ENV para runtime, en Dockerfiles y Docker Compose. Ejemplos, reglas de precedencia, secrets.
Actualizado Publicado 49 min de lectura

Las variables de entorno en Docker parecen simples hasta que un contenedor las ignora silenciosamente. Esta guía cubre las dos instrucciones de Dockerfile que las definen: ARG para build time, y ENV para build time más runtime. También cubre cómo Docker Compose apila interpolación, env_file y reglas de precedencia encima. Tienes ejemplos funcionales para cada uno, más comandos de verificación para correr tras cada cambio.
Todo aquí está comprobado contra el Docker actual (octubre 2026): BuildKit es el builder por defecto desde Engine 23, Compose va por v5.x (el viejo comando docker-compose V1 con guion lleva muerto tiempo), y el campo version: en los archivos compose ahora emite un aviso de obsoleto. Primero el modelo mental, luego snippets copiables, luego los modos de fallo que hacen que las variables de entorno “no funcionen”, incluida la trampa del CMD en forma exec que atrapa a casi todo el mundo.
¿Qué son las variables de entorno en Docker?
Las variables de entorno son pares clave/valor definidos en el entorno de un proceso. Tu aplicación las lee al arrancar (o después) en vez de hardcodear configuración. En Docker hacen el mismo trabajo: una imagen puede apuntar a una base de datos de prueba en un contenedor y a una de producción en el siguiente, sin rebuild.
Docker añade dos instrucciones sobre el env de proceso plano. ARG alimenta valores durante el build de la imagen. ENV define valores que persisten en la imagen y aterrizan en el entorno del contenedor en runtime. También puedes inyectar variables en runtime sin tocar la imagen, con docker run -e o un bloque environment: de Docker Compose. Esa es toda la caja de herramientas.
Dentro del contenedor, cómo lees una variable depende de tu lenguaje: os.environ en Python, process.env en Node, $VAR en un script shell. La variable en sí es la misma en todas partes.
- Build args con
ARGydocker build --build-arg - Config de runtime con
ENVydocker run -e - Variables de entorno en Docker Compose,
env_filee interpolación de.env - Qué valor gana: el orden de precedencia de Compose
- Secrets hechos con seguridad (build secret mounts, Compose secrets)
- Comandos de verificación para cada paso
Más guías de Docker en este sitio:
- añade un usuario no-root a un contenedor Docker
- copia múltiples archivos en una capa de Dockerfile
- instala Docker en Ubuntu
- redirige los logs de Docker a un solo archivo
ARG vs ENV: en qué se diferencian
| ARG | ENV | |
|---|---|---|
| Se define en | Build time | Build time, persiste en runtime |
| Visible en el contenedor corriendo | No | Sí |
Se puede pisar en docker run |
No | Sí, con -e |
| Horneado en la config de la imagen | No | Sí |
Visible en docker history |
Sí, si se usa en una capa | Sí |
Una sutileza que los docs oficiales deletrean: si un ENV y un ARG comparten nombre, el valor ENV gana para cada instrucción RUN posterior. Un --build-arg pasado solo funciona mientras el ARG es la fuente de verdad. Y aunque ARG no se convierte en variable de runtime, los valores usados en capas de build son legibles en docker history. Trátalos como públicos (más en la sección de secrets).
Los dos errores que más cuestan
El CMD en forma exec CMD [“python”, “app.py”] no expande $VAR. Si tu app recibe la cadena literal $DB_URL, es por esto.
Los valores ARG usados en capas de build aparecen en docker history. Nunca pongas contraseñas o tokens en ARG o ENV.
Usando variables ARG en un Dockerfile (build args)
Un ARG declara una variable que solo existe durante docker build. Pasas valores con --build-arg. Si no pasas uno, aplica el default del Dockerfile.
Declara el ARG, refiérelo en instrucciones posteriores, písalo en build time:
# syntax=docker/dockerfile:1
ARG NODE_VERSION=24
FROM node:${NODE_VERSION}-alpine
ARG APP_VERSION=0.0.0
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN echo "Building version ${APP_VERSION}"
CMD ["node", "server.js"]Puedes declarar varios ARGs en una línea (ARG APP_VERSION=0.0.0 APP_ENV=dev) y pasar varios flags --build-arg:
docker build -t my_image \
--build-arg NODE_VERSION=24 \
--build-arg APP_VERSION=1.4.2 .Verifica que el build tuvo éxito, luego comprueba qué se filtró al historial de la imagen:
docker history my_image --no-trunc | grep APP_VERSIONSi el build falla con “empty or invalid base image name”, un ARG global usado en FROM estaba vacío; mira la sección de scope abajo. Para la referencia completa de flags de docker build, mira comandos esenciales de Docker.
Scope de ARG: global vs en-stage (antes y después de FROM)
ARG es la única instrucción permitida antes de FROM, pero también puedes declarar ARGs en cualquier sitio dentro de un stage. La regla que la gente hace mal: un ARG declarado antes de FROM no se hereda al stage. Para usarlo tras FROM, redéclaralo sin default dentro del stage:
# syntax=docker/dockerfile:1
ARG VERSION=latest # global scope, usable in FROM
FROM alpine:${VERSION}
ARG VERSION # bare re-declaration, picks up the global value
RUN echo "$VERSION" # prints the value hereUn ARG declarado dentro de un stage lo heredan los stages hijos basados en ese stage. Los stages no relacionados no pueden verlo. Dos build checks vigilan esta zona: UndefinedArgInFrom marca una variable no declarada usada en FROM, e InvalidDefaultArgInFrom marca un ARG global sin default usable cuando no has pasado uno.
La línea que la gente olvida
El ARG VERSION pelado tras FROM. Sin él, $VERSION dentro del stage está vacío, aunque –build-arg VERSION=1.2.3 funcionó bien en FROM.
Modos de fallo: FROM $VAR con un VAR no declarado dispara UndefinedArgInFrom. Un ARG BASE global sin default y sin --build-arg da “empty or invalid base image name”. Arregla ambos declarando un default (ARG BASE=alpine:3.21) o pasando el flag.
Pisando defaults con docker build --build-arg
--build-arg NAME=value es repetible, un flag por variable. Omite el flag y aplica el default del Dockerfile. Declara un ARG sin default y no pases nada, y el valor es una cadena vacía, normalmente un mal momento.
Dos grupos predefinidos que vale conocer:
- Los args de proxy (
HTTP_PROXY,HTTPS_PROXY,NO_PROXY,ALL_PROXYy variantes en minúsculas) son usables sin declararlos. Están excluidos dedocker historypor defecto. Sáltate la línea explícitaARG HTTP_PROXY: redeclararlo devuelve el valor adocker historyy ata la caché de capas a él. - Los args de plataforma de BuildKit (
TARGETPLATFORM,TARGETOS,TARGETARCH,TARGETVARIANT,BUILDPLATFORM) están disponibles en scope global. Como cualquier ARG global, necesitan una redeclaración pelada dentro de un stage (ARG TARGETARCH) antes de poder usarlos ahí.
Usando variables ENV en un Dockerfile
ENV define variables que persisten en la config de la imagen y aparecen en el entorno de cada contenedor arrancado desde esa imagen. Se definen durante el build y viven en runtime. Esa es toda la diferencia con ARG.
# syntax=docker/dockerfile:1
FROM python:3.13-slim
ENV DB_URL=postgres://user:pass@localhost:5432/app \
API_KEY=changeme \
APP_PORT=8000
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]Notas sobre sintaxis y scope:
- Usa
ENV KEY=value, o varios pares en una línea (ENV A=1 B=2). La vieja forma con espacio (ENV key value) está desaconsejada y puede eliminarse en una versión futura. El build checkLegacyKeyValueFormatla marca. - Usa nombres
UPPER_SNAKE_CASE. Todas las variables que envían Docker y las imágenes base principales son mayúsculas; igualar esa convención ahorra confusión. - ENV se hereda a través de builds multi-stage: “Un stage hereda cualquier variable de entorno definida con ENV por su stage padre o cualquier ancestro”. ARG no. Los stages hijos necesitan su propia declaración.
- El entorno de un contenedor está namespaced por contenedor. Nunca puede sobrescribir las variables de tu host. Lo que corre dentro se queda dentro.
El gotcha clásico de persistencia es DEBIAN_FRONTEND=noninteractive. Si lo horneas como ENV, se queda en la imagen final y sorprende a cada usuario interactivo después. Ponlo inline en el RUN en su lugar:
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*Si quieres un servicio Python completo funcionando con este patrón, mira cómo correr una app Python en Docker con Docker Compose.
Por qué $VAR no se expande en CMD en forma exec
Esta es la razón única más común de “las variables de entorno de Docker no funcionan”. La forma exec corre sin shell:
CMD ["python", "app.py", "$DB_URL"]La app recibe la cadena literal $DB_URL. Sin expansión, sin pipes, sin &&. La forma shell expande, pero entonces /bin/sh es PID 1 y se traga SIGTERM, así que docker stop espera diez segundos y mata el contenedor a las malas. Tres opciones, en orden de preferencia:
Leer env en la app (recomendado)
ENV DB_URL=postgres://user:pass@localhost:5432/app
CMD ["python", "app.py"]import os
db_url = os.environ["DB_URL"]La forma exec se queda, el manejo de señales funciona, y la app obtiene la variable de su propio entorno. Así se comportan las imágenes oficiales.
Forma shell (rápido)
CMD python app.py "$DB_URL"La expansión funciona. El truco: /bin/sh se convierte en PID 1 y no reenvía señales a Python. Aceptable para jobs cortos, descuidado para un servicio web longevo.
Script wrapper (lo mejor de ambos)
COPY --chmod=755 <<EOT /entrypoint.sh
#!/bin/sh
set -e
exec python app.py "$DB_URL"
EOT
ENTRYPOINT ["/entrypoint.sh"]El shell expande la variable, exec reemplaza el shell con Python, y Python se convierte en PID 1. Las señales vuelven a funcionar.
Por esto tu contenedor ignora $DB_URL
El CMD en forma exec CMD [“python”, “app.py”, “$DB_URL”] pasa el texto literal $DB_URL a la app. Usa una de las tres opciones de arriba. El build check JSONArgsRecommended avisa de la mitad de señales de este problema; corre docker build –check . para verlo.
Modos de fallo: la app loguea un error de conexión que contiene $DB_URL o falla al parsearlo como URL, lo que significa forma exec. docker stop se cuelga diez segundos, lo que significa forma shell sin exec. Arreglo: lee la variable dentro de la app, o usa el script wrapper.
Pisando ENV en runtime con docker run -e
El ENV de la imagen es un default, no una sentencia. docker run -e lo pisa por contenedor:
docker run -d --name app -p 8000:8000 \
-e DB_URL=postgres://user:pass@db:5432/app \
-e API_KEY=supersecret \
my_imageDos formas, comportamiento distinto:
-e DB_URL=valuedefine o pisa la variable solo para este contenedor.-e DB_URL(sin=) pasa el valor de tu shell actual. Si está unset ahí, la variable está ausente en el contenedor.
Ninguna forma toca el entorno del host. docker run -e solo escribe valores en el contenedor.
Para muchas variables, usa un archivo:
docker run --env-file env.list my_image# env.list
# full-line comments only; a # anywhere else is literal text
DB_URL=postgres://user:pass@db:5432/app
API_KEY=supersecret
USERUna línea USER pelada extrae ese valor de tu entorno local. No hay interpolación ${VAR} en docker run --env-file; eso es una función solo de Compose.
Verifica lo que recibió el contenedor:
docker exec app printenv | grep DB_URLSi falta la línea, la variable nunca entró. Revisa los flags de docker run y la config de la imagen con docker inspect (mira la sección de verificación).
Variables de entorno en Docker Compose
Compose es donde la mayoría de gente se encuentra con las variables de entorno de Docker en serio, porque el mismo valor aparece en cuatro sitios: el shell, un archivo .env, el archivo compose y la imagen. Primero, dos notas de modernización.
Borra version: de tu archivo compose
Compose valida con el esquema más nuevo sin importar el campo version:. Mantenerlo solo imprime un aviso de obsoleto. Usa también el plugin docker compose (comprueba con docker compose version; v5.x a octubre de 2026), no el muerto docker-compose V1 con guion.
Un servicio mínimo con env vars explícitas:
services:
app:
image: my_image
ports:
- "8000:8000"
environment:
DB_URL: postgres://user:pass@db:5432/app
API_KEY: supersecretCuando despliegas stacks reales así, un panel de control ayuda. Yo corro apps Docker Compose en un VPS a través de Dokploy, y los archivos env son parte de cada deploy; mira cómo desplegar una app Docker Compose en Dokploy.
Definiendo vars con environment: y env_file:
environment: acepta forma lista o mapa. La forma mapa es la que uso; se lee mejor y hace los overrides obvios:
# list form
environment:
- DB_URL=postgres://user:pass@db:5432/app
- API_KEY=supersecret# map form (same values, easier to override one key)
environment:
DB_URL: postgres://user:pass@db:5432/app
API_KEY: supersecretenv_file: carga un archivo entero. Las rutas son relativas al archivo compose. Con varias entradas, los archivos posteriores pisan a los anteriores:
services:
app:
image: my_image
env_file:
- ./app.env
- ./overrides.envLa sintaxis larga vale conocerla para archivos opcionales:
env_file:
- path: ./app.env
required: false # silently ignored when missing (Compose >= v2.24.0)
- path: ./extra.env
required: true
format: yaml # alternative format (Compose >= v2.30.0)Overrides puntuales sin editar nada: docker compose run -e DEBUG=1 app o docker compose exec -e DEBUG=1 app. Las imágenes oficiales como Postgres y MySQL también aceptan una variante _FILE de sus variables (POSTGRES_PASSWORD_FILE); más en la sección de secrets.
Interpolación y archivos .env
La interpolación ocurre en la CLI de Compose cuando parsea el archivo compose. No es una función de Docker Engine. Sintaxis soportada:
image: webapp:${TAG} # value from shell or .env
environment:
LOG_LEVEL: ${LOG_LEVEL:-info} # default if unset OR empty
MODE: ${MODE-dev} # default only if unset
DB_URL: ${DB_URL:?set DB_URL} # fail if unset or empty
BANNER: ${MSG:+configured} # alternative if setUn archivo .env en el directorio del proyecto alimenta la interpolación automáticamente. Reglas de parseo que hacen tropezar:
TAG=1.4.2
DB_URL=postgres://user:pass@db:5432/app
LITERAL='${TAG}' # single quotes: literal, no interpolation
EXPANDED="${TAG}" # double quotes: interpolation and \n \t escapes work
LOG_LEVEL=debug # inline comments need a leading space before #Dónde busca Compose los valores de interpolación, de mayor a menor: tu entorno de shell, luego archivos --env-file (el último gana), luego el .env del proyecto. Dos límites de scope: docker run --env-file no interpola, y tampoco lo hace docker stack deploy de Swarm.
Modo de fallo: un marcador ${VAR} llegó a la app como texto literal. O lo usaste en docker run --env-file (no soportado) o el valor del .env está entre comillas simples.
Qué valor gana: orden de precedencia de Compose
Para el entorno de un contenedor corriendo, de mayor a menor:
docker compose run -e VAR=...- Entradas
environment:/env_file:cuyo valor se interpoló del shell o.env environment:en el archivo composeenv_file:en el archivo composeENVde la imagen desde el Dockerfile
El gotcha que muerde a todos: una variable de host que nunca es referenciada por environment:, env_file: o run -e no llega al contenedor. Compose no autoinyecta tu entorno de shell. Si tu contenedor ignora una variable que exportaste, es por esto.
Verifica el resultado resuelto antes de desplegar:
docker compose config
docker compose config --environment # if your Compose version supports itdocker compose config imprime el archivo compose efectivo tras la interpolación y la fusión de env_file. Si el valor está mal ahí, el contenedor también estará mal.
Usando ARG y ENV juntos
El patrón puente: declara un ARG, cópialo en un ENV, y el argumento de build se convierte en el default de runtime.
# syntax=docker/dockerfile:1
ARG APP_PORT=8080
ENV APP_PORT=${APP_PORT}
CMD ["node", "server.js"]La app lee process.env.APP_PORT, sin expansión de forma exec implicada (mira el arreglo arriba). Buildea con --build-arg APP_PORT=9090 y la imagen sale con APP_PORT=9090 como default. Corre con -e APP_PORT=3000 y eso sigue ganando para el contenedor.
Sé preciso sobre lo que esto hace: los valores que puentees con ENV están en el entorno de runtime (ese es el punto del puente). Los valores que dejas solo como ARG no, y solo son usables en instrucciones de build.
Verifica el default horneado:
docker build -t my_image --build-arg APP_PORT=9090 .
docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' my_image | grep APP_PORT
# APP_PORT=9090Los docs oficiales usan la misma forma: ENV CONT_IMG_VER=${CONT_IMG_VER:-v1.0.0} con un ARG encima. Fíjate en la interacción: después de definirse ENV APP_PORT=..., un RUN posterior que lea $APP_PORT ve el valor ENV aunque --build-arg cambiara el ARG.
Pasando secrets con seguridad: no uses ARG ni ENV para credenciales
Las variables de entorno son cómodas y con fugas. Cualquiera que pueda correr docker inspect sobre el contenedor las ve, igual que docker exec printenv y /proc/<pid>/environ. ARG es peor: los valores usados en capas de build se quedan en docker history para siempre, y las attestations de procedencia también pueden portarlos. Los propios docs de ARG de Docker lo dicen claramente: no se recomienda usar build arguments para pasar secrets.
Nunca pongas contraseñas o tokens API en ARG o ENV
Acaban en docker history y docker inspect. Usa build secret mounts y Compose secrets en su lugar.
En build time, pasa secrets como mounts que nunca entran en una capa:
# syntax=docker/dockerfile:1
FROM alpine:3.21
# secret as a file at /run/secrets/API_TOKEN
RUN --mount=type=secret,id=API_TOKEN \
./fetch-artifact.sh "$(cat /run/secrets/API_TOKEN)"
# or mounted straight into an env var for that single RUN
RUN --mount=type=secret,id=AWS_ACCESS_KEY_ID,env=AWS_ACCESS_KEY_ID \
--mount=type=secret,id=AWS_SECRET_ACCESS_KEY,env=AWS_SECRET_ACCESS_KEY \
aws s3 cp s3://my-bucket/file .docker build --secret id=API_TOKEN . # from local env var API_TOKEN
docker build --secret id=AWS_ACCESS_KEY_ID,src=./aws_key . # from a file
docker build --secret id=aws,target=/root/.aws/credentials .
docker build --ssh default=. # private repos over SSH agentLos secrets van por defecto a /run/secrets/<id>; target= renombra la ruta y env= lo monta como variable de entorno en vez de archivo.
En runtime, los secrets de Compose funcionan sin Swarm. Define el secret a nivel top, adjúntalo al servicio, y aparece como archivo en el contenedor:
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtLa convención _FILE (POSTGRES_PASSWORD_FILE, MYSQL_ROOT_PASSWORD_FILE) es como las imágenes oficiales esperan esto. Los secrets de Docker planos son solo para contenedores Linux (archivos individuales bind-mounted). Para más opciones, incluyendo qué funciona y qué no, mira secrets en Docker Compose.
Los build checks de BuildKit pueden marcar los viejos hábitos: corre docker build --check . y busca SecretsUsedInArgOrEnv, que coincide con nombres de clave como *TOKEN* y AWS_*_KEY.
- Secrets de build vía
docker build --secretyRUN --mount=type=secret - Secrets de runtime vía
secrets:de Compose montados en/run/secrets/NAME - Variantes
_FILEdonde la imagen las soporta docker build --check .antes de publicar una imagen- Nada sensible en un archivo
.envque git pueda ver
Coste y ops: los secret mounts no cuestan nada extra en runtime. La restricción es operativa: el archivo de secret debe existir en build o deploy time, así que en un VPS guárdalo fuera del repo (propiedad de root, modo 600) o en un gestor de secrets. Si docker history sigue mostrando tu token, usaste ARG o ENV. Si /run/secrets está vacío en el contenedor, falta la entrada secrets: a nivel de servicio. Si el build no encuentra un secret, la ruta src= está mal.
Cómo verificar tus variables de entorno
No adivines. Tras cada cambio, comprueba lo que tiene el contenedor:
- Para el env del contenedor vivo, corre
docker exec <container> printenv. Imprime todo lo que el proceso ve, una línea por variable. - Para la config de la imagen o contenedor, corre
docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' <container-or-image>. Esto es lo que Docker registró en create time, independiente de mutaciones de runtime. - Para lo que se filtró a las capas, corre
docker history my_image --no-trunc. Busca build args y valores ENV en el historial de la imagen. - Para la config de Compose resuelta, corre
docker compose config. Imprime el archivo efectivo tras interpolación y fusión deenv_file.docker compose config --environmentlista las variables interpoladas si tu versión de Compose lo soporta;docker compose configa secas funciona en todas partes. - Para un lint del Dockerfile, corre
docker build --check .. Atrapa los errores habituales de ARG/ENV:SecretsUsedInArgOrEnv,JSONArgsRecommended,UndefinedArgInFrom,InvalidDefaultArgInFrom,UndefinedVar,LegacyKeyValueFormat.
Si printenv no coincide con tu archivo compose, la escalera de precedencia de arriba decide quién ganó; docker compose config muestra la misma respuesta desde el lado Compose. Una referencia de comandos más amplia está en comandos esenciales de Docker.
Buenas prácticas para variables de entorno en Docker
- Usa ARG para valores de build time (números de versión, plataformas objetivo), nunca para credenciales.
- Usa ENV para config de runtime que la app lee; mantén la app leyendo
os.environ/process.env. - Nunca pongas contraseñas o tokens en ARG o ENV; usa secret mounts y Compose secrets.
- Nombra todo
UPPER_SNAKE_CASEy mantén nombres consistentes entre servicios. - Documenta las variables requeridas con un archivo
env.exampleen el repo (nombres y valores dummy, los valores reales fuera). - Da a las vars no sensibles defaults sensatos, ya sea en
ENVo con sintaxis de default estilo compose. - Usa
.envpara desarrollo local. No lo commitees, y no lo hagas tu único mecanismo de producción.
Esto no cuesta nada en deploy time y previene los dos incidentes que más aparecen: credenciales filtradas y contenedores que ignoran su config en silencio.
Problemas comunes con variables de entorno en Docker
Síntoma, causa, arreglo:
- La app recibe la cadena literal
$DB_URL. Causa: el CMD en forma exec no expande variables. Arreglo: lee la env var en la app, usa forma shell, o un script wrapper conexec. --build-argno tuvo efecto dentro de mi stage. Causa: el ARG se declaró antes deFROMy nunca se redeclaró en el stage. Arreglo: añade unARG NAMEpelado trasFROM.- Compose ignora mi variable de entorno del host. Causa: las vars de host no referenciadas nunca llegan al contenedor. Arreglo: declárala en
environment:oenv_file:. ${VAR}llegó como texto literal. Causa: la interpolación es una función de la CLI de Compose (no funciona endocker run --env-fileni en stacks Swarm), o el valor del.envestá entre comillas simples. Arreglo: interpola en el archivo compose, no en el archivo env.- El nuevo valor no aplicó a un contenedor corriendo. Causa: el env se fija cuando se crea el contenedor. Arreglo:
docker compose up -dpara recrear, odocker rm -fy corre de nuevo. Un reinicio a secas no siempre basta si la config de la imagen en sí cambió. - Gana el valor equivocado. Causa: confusión de precedencia. Arreglo:
docker compose configy lee el valor resuelto; compara contra la escalera de arriba. - “Docker sobrescribió una variable en mi host.” No puede. El env del contenedor está namespaced por contenedor. Lo que viste es la dirección contraria:
docker run -e VARsin=pasa el valor del host al contenedor.
Cuando el contenedor se porta mal tras un cambio de env, los logs ayudan: redirige los logs de Docker a un solo archivo y busca el nombre de variable que tu app loguea al arranque.
FAQ
¿Las variables ENV de Docker persisten después de parar el contenedor?
Los valores ENV horneados en la imagen persisten mientras la imagen lo haga. Los valores pasados con docker run -e pertenecen solo a ese contenedor. Si eliminas y recreas el contenedor, debes pasarlos otra vez. Si horneas secrets como ENV, persisten en la imagen, que es exactamente el problema. Usa secret mounts en su lugar.
¿Cuál es la diferencia entre ENV y ARG en Dockerfile?
ARG es solo build-time y no aparece en el entorno del contenedor. ENV se define en build time y persiste en runtime. Los valores ARG usados en capas siguen siendo visibles en docker history, así que ARG no es un mecanismo de secrets. Para puentearlos, ENV MI_VAR=${MI_ARG} copia el valor de build en el default de runtime.
¿Puedo usar archivos .env con docker run?
Sí, con docker run –env-file env.list. El formato son líneas planas KEY=value y nombres KEY pelados que extraen de tu shell local. Solo comentarios # de línea completa. No hay interpolación ${VAR} en ese archivo; la interpolación es una función de la CLI de Compose.
¿Dónde viven los secrets de Docker Compose?
En /run/secrets/<nombre> dentro del contenedor, como un archivo individual bind-mounted. Esto funciona con Docker Compose plano en contenedores Linux, sin Swarm requerido. Las imágenes que siguen la convención _FILE (Postgres, MySQL) los leen directamente vía POSTGRES_PASSWORD_FILE y amigos.
¿Por qué es obsoleto version: en mi archivo compose?
Compose valida cada archivo con el esquema más nuevo sin importar ese campo, así que solo imprime un aviso. Bórralo. Confirma también que estás en el plugin docker compose (docker compose version, actualmente v5.x) en vez del eliminado comando V1 docker-compose.
Conclusión
El modelo mental es pequeño: ARG alimenta el build, ENV alimenta el build y el contenedor, y Docker Compose añade interpolación más un orden de precedencia que siempre puedes resolver con docker compose config. Los dos errores caros son el CMD en forma exec comiéndose tus variables y las credenciales en ARG o ENV filtrándose por docker history. Los secret mounts arreglan ambos. Corre los comandos de verificación tras cada cambio y “ignora mi env var” deja de ser un misterio.
Todo lo de arriba corre igual en un VPS pequeño, que es como despliego la mayoría de estos stacks. Hetzner Cloud es mi default cuando necesito una máquina EU barata y fiable para Docker y Compose.
Explora más guías de Docker

