Secrets Compose para un agente IA: no en el env del proceso
Resumen
Cómo no dejar el token de Telegram en docker inspect: secrets de Compose con file o environment (v2.6.0+), mount /run/secrets, grant por servicio. Distinto de env y de Swarm 500 KB. uid/mode no aplican con file en Compose. Techos oficiales curl 3 de septiembre de 2026.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
environment: TELEGRAM_TOKEN: $TOKEN aparece en docker inspect (JSON del objeto, campo Config.Env) y en dumps de crash. Compose secrets montan un archivo de solo lectura (corto: /run/secrets/<nombre>). El proceso lee el path. Fuentes oficiales consultadas el 3 de septiembre de 2026.
La guía de env de Compose lo dice sin rodeos: no pases contraseñas por variables; usa secrets. Esta pieza es el transporte en un VPS con Compose v2, no el criterio de “qué es secreto” (secretos y variables).
La regla
Dos pasos. Primero el top-level. Después el grant explícito en el servicio. Declarar el secret no lo monta en nadie.
secrets:
telegram_token:
file: ./secrets/telegram_token
services:
agent:
image: agent:pinned
secrets:
- telegram_token
El código: readFileSync("/run/secrets/telegram_token","utf8").trim(). No process.env.TELEGRAM_TOKEN para el secreto largo.
Vars no secretas (PORT, NODE_ENV) sí van en env. NODE_ENV no es un token.
File vs environment vs Swarm
El spec top-level (curl 2026-09-03): la fuente es file o environment.
file:contenido del path en el host. Sin Swarm. Es el default de un VPS.environment:valor de una variable del host donde corre Compose. Badge oficial Compose v2.6.0.docker stack deployno lo soporta: usafileoexternal.external: true: ya existe en la plataforma; Compose no lo crea; si falta, error. Siexternales true, cualquier otro atributo distinto denameinvalida el yaml.name:el objeto en Docker tal cual, sin prefijo de proyecto.
Swarm (docker secret create) es otro API: cifrado en tránsito y en reposo, solo services de swarm (no contenedores standalone), blob hasta 500 KB. No lo copies a un Compose de laptop. El how-to de Compose v2 con file: no exige cluster.
El nombre registrado al desplegar un file no-external es <proyecto>_<secret> (ejemplo oficial server-certificate → <project>_server-certificate). Dentro del contenedor igual ves /run/secrets/telegram_token si usas la sintaxis corta.
Sintaxis corta vs larga
Corta: solo el nombre. Mount read-only en /run/secrets/<secret_name>. Source y destino = el nombre.
Larga (spec services, curl 2026-09-03):
| Campo | Qué hace | Default |
|---|---|---|
source | nombre del secret en la plataforma | — |
target | fichero bajo /run/secrets/ o path absoluto | = source |
uid / gid | dueño numérico del fichero | — |
mode | octal. Default 0444 (world-readable). El bit de escritura se ignora. El de ejecución puede ir. | 0444 |
Nota oficial: uid, gid y mode no están implementados en Docker Compose cuando la fuente es file, porque el bind-mount no remapea uid. El ejemplo con uid: "103", gid: "103", mode: 0o440 usa external: true (plataforma Swarm/lookup). En un VPS con file:, no cuentes con 0440: el uid del proceso no-root tiene que poder leer el bind.
Linux only en Compose: un secret es un archivo bind-mounted. Windows solo monta directorios, así que Compose no entrega secrets a contenedores Windows.
Relación con variables
env_file: .env sigue siendo inspectable (Config.Env). No lo uses para el token. Desde Compose 2.24.0, required: false en env_file silencia un archivo ausente: útil para un .env de debug, no para el secreto.
La interpolación de .env es del CLI de Compose, no de docker run --env-file.
No pases el secret como environment “y también file”. El inspect gana.
Postgres oficial (Hub, curl 2026-09-03): convención _FILE. Soporta solo POSTGRES_INITDB_ARGS, POSTGRES_PASSWORD, POSTGRES_USER, POSTGRES_DB. Ejemplo: POSTGRES_PASSWORD_FILE=/run/secrets/postgres-passwd. El how-to de Compose muestra lo mismo con MySQL (MYSQL_ROOT_PASSWORD_FILE, MYSQL_PASSWORD_FILE). El agente no copia POSTGRES_PASSWORD a su env: lee su propio secret.
Build vs runtime
Runtime: services.agent.secrets. Build: otro canal. El how-to muestra build.secrets + top-level npm_token: environment: NPM_TOKEN. Eso alimenta RUN --mount=type=secret en el Dockerfile; no deja el token en la imagen si el mount es de un RUN. No mezcles el token de Telegram de runtime con el de npm del build.
Fly / Railway / Workers
No es /run/secrets. Fly (docs de apps/secrets, curl 2026-09-03; la URL /docs/reference/secrets/ redirige): fly secrets va a un vault cifrado; el API solo cifra, no descifra; al arrancar la Machine el host recibe un token temporal, el agente inyecta variables de entorno. Destruir la Machine corta el acceso. Railway/Vercel: env del dashboard. Workers: secrets de CF. dockerignore cubre el contexto de imagen, no el vault de Fly.
Tabla

| Dato | Dónde | Visible inspect Config.Env | Techo verificado |
|---|---|---|---|
| TELEGRAM_TOKEN | secret file: | no | path /run/secrets/<name> |
| OPENAI_KEY | secret file: | no | grant por servicio |
fuente environment: | host env → secret | no en el contenedor como env | Compose ≥ 2.6.0; no stack deploy |
| Swarm blob | docker secret | n/a | 500 KB; solo swarm services |
| mode default | long syntax | n/a | 0444; write bit ignorado |
uid/gid/mode + file: | Compose v2 | n/a | no implementado |
| POSTGRES_PASSWORD | POSTGRES_PASSWORD_FILE | el path, no el valor | 4 vars _FILE en la imagen oficial |
| PORT / NODE_ENV | env | sí, ok | no son secretos |
| webhook URL pública | env o config | sí, ok | no es un token |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
| inspect lista el token | environment / env_file | secrets + leer file |
ENOENT /run/secrets | top-level sin grant en el servicio | secrets: en agent |
| newline en token | file con \n | .trim(); printf '%s' |
| secret en git | path bajo repo | gitignore + no COPY |
Swarm external en laptop | copy/paste | file: local |
uid: "103" ignorado | fuente file: en Compose | mismo uid que USER |
| Windows container | bind de archivo no soportado | Linux |
external + file juntos | spec: inválido | o external+name, o file |
| token en la imagen | ENV/ARG en build | RUN --mount=type=secret |
Relación con el resto
- Profiles: compose profiles — Adminer off no borra el secret top-level (networks/volumes/secrets siguen definidos).
- Redes: compose networks.
- Usuario: no-root — el uid debe leer el bind.
- CI: Actions — escribe el file en el runner, no lo echo al log.
- Logs: json-file si el token ya no está en env y igual sale.
Checklist
- Token y API keys como
secrets:con grant en el servicio - Código lee path, no env
-
.trim() -
secrets/gitignored - Ensayo:
docker inspect/--format '{{json .Config.Env}}'sin el token - uid no-root puede leer el file (no cuentes uid/mode con
file:) - Postgres usa
_FILEen las 4 vars soportadas, no env del agente - Prod sin
environment:de Compose para el token (host env ≠ env del contenedor)
FAQ
¿Workers? Secrets de CF, no Compose.
¿Vercel? Env del dashboard; no /run/secrets.
¿Rotación? Reescribes el file del host + compose up -d --force-recreate agent. Compose no recarga el bind en un contenedor vivo. Un SIGHUP casero no está en el contrato.
¿Varios secrets? Lista, no un JSON único. Un leak de un key no tira todos.
¿echo $TOKEN > secrets/telegram_token? Queda en bash history. printf '%s' "$TOKEN" sin newline.
¿Montar el directorio secrets/ como volume rw? El contenedor podría reescribir. El mount de Compose secrets es un archivo, no un dir rw.
Un secret no es magia: root del host lo lee. Es no filtrarlo a logs/inspect/Sentry extra.
El ensayo: token dummy. inspect no lo lista en Env. El bot arranca leyendo el file. compose exec agent ls -l /run/secrets.
Siguiente paso: si el token ya no está en env y igual sale en logs, json-file y no loguees headers. Sin runtime: curso.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



