Guía9 min

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.

Docker
Token del agente en un archivo secret montado, no en variables de entorno

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 deploy no lo soporta: usa file o external.
  • external: true: ya existe en la plataforma; Compose no lo crea; si falta, error. Si external es true, cualquier otro atributo distinto de name invalida 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):

CampoQué haceDefault
sourcenombre del secret en la plataforma
targetfichero bajo /run/secrets/ o path absoluto= source
uid / giddueño numérico del fichero
modeoctal. 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

Secreto en archivo versus env

DatoDóndeVisible inspect Config.EnvTecho verificado
TELEGRAM_TOKENsecret file:nopath /run/secrets/<name>
OPENAI_KEYsecret file:nogrant por servicio
fuente environment:host env → secretno en el contenedor como envCompose ≥ 2.6.0; no stack deploy
Swarm blobdocker secretn/a500 KB; solo swarm services
mode defaultlong syntaxn/a0444; write bit ignorado
uid/gid/mode + file:Compose v2n/ano implementado
POSTGRES_PASSWORDPOSTGRES_PASSWORD_FILEel path, no el valor4 vars _FILE en la imagen oficial
PORT / NODE_ENVenvsí, okno son secretos
webhook URL públicaenv o configsí, okno es un token

Errores comunes

Token en docker inspect

SíntomaCausaFix
inspect lista el tokenenvironment / env_filesecrets + leer file
ENOENT /run/secretstop-level sin grant en el serviciosecrets: en agent
newline en tokenfile con \n.trim(); printf '%s'
secret en gitpath bajo repogitignore + no COPY
Swarm external en laptopcopy/pastefile: local
uid: "103" ignoradofuente file: en Composemismo uid que USER
Windows containerbind de archivo no soportadoLinux
external + file juntosspec: inválidoo external+name, o file
token en la imagenENV/ARG en buildRUN --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 _FILE en 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.