Build secrets Docker para un agente IA: el token no va en el layer
Resumen
Cómo instalar un paquete privado en el build del agente sin hornear el token: RUN --mount=type=secret, --secret id= en Buildx y secrets/secret-envs/secret-files en GitHub Actions. Distinto de Compose secrets (runtime) y de .dockerignore. Default /run/secrets/<id>, required=true.

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.
ARG NPM_TOKEN y ENV TELEGRAM_TOKEN sobreviven en docker history. Un COPY .npmrc . mete el token en un layer aunque lo borres después. El build del agente sí necesita secretos (registry privado, Sentry auth, AWS para un aws s3 cp de un modelo). La vía correcta no es ARG: es un secret mount de BuildKit que vive solo durante ese RUN y no se persiste en la imagen.
Esta guía cubre el build. El runtime va en Compose secrets. No copiar .env al contexto va en dockerignore. Qué es un secreto y cómo rotarlo, en secretos y variables.
La regla
El Dockerfile declara el id. El cliente expone el valor con --secret. El RUN lo lee de un archivo (default /run/secrets/<id>) o de una env del paso, no de la imagen. Si el valor no llega y pones required=true, el build falla cerrado. Si no pones required, el mount vacío es un silencio caro: npm ci contra el registry público y un agente a medias.
Necesitas # syntax=docker/dockerfile:1 arriba del Dockerfile. Sin frontend moderno, --mount=type=secret no existe.
Qué monta BuildKit
Opciones oficiales de RUN --mount=type=secret (Dockerfile reference, consultada el 7 de septiembre de 2026):
| Opción | Default | Para qué |
|---|---|---|
id | basename del target | El mismo id que pasas a --secret |
target / dst / destination | /run/secrets/<id> si no hay env | Path del archivo en el contenedor de build |
env | (off) | Carga el secreto a una variable solo en ese RUN (desde Dockerfile v1.10.0) |
required | false | Falla si el secreto no está |
mode | 0400 | Permisos del archivo |
uid / gid | 0 | Dueño del archivo |
El valor no queda en el layer. En Linux, mode 0400 + uid del usuario del RUN evita lecturas extra. En Windows no hay /run/secrets/ por defecto: target explícito.
Ejemplo: npm privado en el build del agente
El agente instala un SDK interno. El token no puede ir al blob.
# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=secret,id=npm_token,required=true \
sh -c 'printf "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)\n" > /tmp/.npmrc \
&& pnpm install --frozen-lockfile --config.npmrc=/tmp/.npmrc \
&& rm -f /tmp/.npmrc'
Cliente local:
docker buildx build --secret id=npm_token,src="$HOME/.npm_token" .
Buildx detecta type=file si no hay env con el mismo nombre que id. Si NPM_TOKEN está en el entorno y el id es NPM_TOKEN, detecta type=env. Explícito siempre:
docker buildx build --secret id=npm_token,env=NPM_TOKEN .
No uses ARG + echo $NPM_TOKEN > .npmrc. Ese string entra al historial del layer.
ARG vs secret vs Compose
| Mecanismo | Vive en | Sale en docker history | Uso |
|---|---|---|---|
ARG / --build-arg | metadata del build | sí (y provenance max) | flags no secretos (NODE_ENV de build) |
ENV | imagen final | sí | config pública del runtime |
RUN --mount=type=secret | solo ese RUN | no | token de registry, clave de firma |
Compose secrets: | contenedor runtime | no (si no lo echoas) | token del bot en producción |
Compose secrets no alimentan el build. Un docker compose build sin --secret deja el RUN --mount=type=secret vacío. Si el agente se construye en CI y corre en un VPS, son dos inyecciones: esta guía para la imagen, Compose secrets para el proceso.
El multi-stage no salva un leak: COPY --from=deps copia lo que ya quedó en deps. El secret mount tiene que estar en el RUN que consume el token, no “en el stage” como idea vaga.

GitHub Actions: tres inputs, un id
La página oficial Using secrets with GitHub Actions (verificada el 7 de septiembre de 2026) separa de dónde sale el valor (input de docker/build-push-action) de cómo lo consume el Dockerfile (RUN --mount).
| Input de la action | Fuente | Equivalente Buildx |
|---|---|---|
secrets: ID=value | string del workflow | --secret id=ID,src=<temp> |
secret-envs: ID=ENV_VAR | env del runner | --secret id=ID,env=ENV_VAR |
secret-files: ID=./file | archivo en el runner | --secret id=ID,src=./file |
Patrón mínimo con GITHUB_TOKEN (el id del mount es github_token; el valor viene del contexto secrets):
- uses: docker/build-push-action@v7
with:
tags: user/agente:latest
secrets: |
github_token=${{ secrets.GITHUB_TOKEN }}
# syntax=docker/dockerfile:1
FROM alpine
RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \
wget --header="Authorization: Bearer $GITHUB_TOKEN" ...
secret-envs cuando un step previo ya exportó la variable:
- uses: docker/build-push-action@v7
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
secret-envs: |
sentry_token=SENTRY_AUTH_TOKEN
secret-files para un .npmrc o .pypirc ya formateado. GitHub Docs: no pases secretos por argv (ps los ve); usa env, STDIN o el mount. Un secret no seteado en Actions es string vacío, no error: por eso required=true en el Dockerfile.
Forks no reciben secrets (salvo GITHUB_TOKEN limitado). Un PR de fork que construye el agente no debe asumir NPM_TOKEN. Para la nube, OIDC evita una clave eterna.
Lo que el mount no cubre
- Logs.
cat /run/secrets/npm_tokenen el Dockerfile imprime el token. Actions enmascara${{ secrets.* }}en logs; no enmascara lo que túechodesde el archivo montado. Ceroset -xalrededor delcat. - Cache de BuildKit. El resultado del
RUNsí se cachea. El secreto no va en la clave como texto, pero un layer cacheado connode_modulesde un registry privado es el artefacto, no el token. No “invalides cache pegando el token al comando”. - Provenance.
--build-argde un token sale en attestations modomax. El secret mount no. Si publicas SBOM/attestations, ARG de credenciales es un leak distinto al del layer. - SSH.
RUN --mount=type=sshes otro tipo (agente o clave). No lo mezcles contype=secretni con clonar por HTTPS + token en la URL. - Runtime del bot. El Telegram token no se “build-sea”. Se monta en el contenedor que corre. Mezclarlos es el incidente clásico: imagen pública con el bot token en un layer “temporal” que no lo era.

Checklist
- Primera línea:
# syntax=docker/dockerfile:1. - Cada
RUNque habla con un registry privado usa--mount=type=secret,id=…,required=true. - Default path
/run/secrets/<id>oenv=en el mismoRUN. NuncaENV TOKEN=. - Cliente:
docker buildx build --secret id=…,src=…oenv=…. Compose build no basta. - CI:
secrets/secret-envs/secret-filesdebuild-push-action; el id coincide con el Dockerfile. grepdel Dockerfile: ceroARGde tokens, ceroCOPY .npmrc, ceroecho $TOKEN.- Tras el build:
docker historyydocker run --rm imagen cat /run/secrets/npm_tokendeben fallar (el path no existe en runtime). - Token del bot: Compose secrets o el secret store de Fly/Railway, no este mount.
FAQ
¿Puedo usar el mismo id en build y en Compose? Sí como nombre. No es el mismo mecanismo. BuildKit lo monta en el contenedor de build; Compose, en el de run.
¿env= deja el token en la imagen? No. env del mount vale solo para ese RUN. ENV del Dockerfile sí queda. No los confundas.
¿Y si el agente es serverless (Vercel/Workers)? No hay Dockerfile. El secreto de build es el de la plataforma (env de build), no un mount. Esta guía es para imagen Docker del webhook/worker self-hosted.
¿El secret file en el runner queda en el workspace? secret-files apunta a un path que tú creaste. Bórralo en el mismo job. secrets: inline lo escribe Buildx a un temp y no lo deja en el checkout.
Siguiente paso
Pon required=true en el RUN que instala dependencias privadas y deja de pasar --build-arg del token. Si el agente aún no tiene imagen, el curso instalar un agente arma el runtime; el hub seguridad, coste y operación agrupa el resto del deploy.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.

SBOM y attestations Docker para un agente IA: qué hay dentro, cómo se construyó

Docker Bake para un agente IA: un archivo, varios targets, cero CLI eterno

Compose Watch para un agente IA: sync no es bind mount
