.dockerignore para un agente IA: el .env no va en la imagen
Resumen
Cómo no hornear TELEGRAM_TOKEN en un layer: .dockerignore, COPY selectivo y build context mínimo. docker history enseña el secreto. Inyecta env en runtime (Railway, Fly, Vercel), no en el build. El digest pineado no salva un COPY de todo el repo.

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.
COPY . . mete .env, .git, node_modules locales y a veces un dump de sqlite en la imagen. Luego publicas el tag. Secretos dicen cómo inyectar en runtime. Esta guía cubre no copiarlos al blob. Fuentes oficiales consultadas el 3 de septiembre de 2026.
La regla
El contexto de build es un tarball. .dockerignore lo recorta antes de COPY. Si el archivo llegó al daemon, ya está en un layer aunque lo borres después (docker history sigue mostrando el leak en capas viejas, incluso si “aplastas” la imagen).
Ignore: .env*, .git, *.db, *.db-wal, node_modules, .DS_Store, secrets/. COPY solo package.json, lockfile y src.
Context
El cliente de build busca .dockerignore en la raíz del contexto. Si existe, recorta los matches antes de mandar el tarball al builder. Un repo de 2 GB con data/ mata el build y el disco (ENOSPC). No es estética; es el firewall del build.
COPY . . + ignore incompleto = incidente. Lista blanca > lista negra cuando puedas: copia paths explícitos.
Si tienes varios Dockerfiles, el ignore específico gana: build.Dockerfile.dockerignore en el mismo directorio que build.Dockerfile pisa al .dockerignore de la raíz. Un preview que construye con -f docker/prod.Dockerfile y un ignore genérico en la raíz no es el mismo recorte.
Patrones: globs estilo Unix; barras al inicio y al final se ignoran. # en columna 1 es comentario. **/*.db cubre cualquier profundidad. La última línea que matchea decide: *.md + !README.md deja el README; si README-secret.md va después del !, ese archivo vuelve a salir.
COPY no puede subir de la raíz del contexto: COPY ../algo /algo se recorta a algo. El token que está un directorio arriba del contexto no entra “por accidente”; entra si el contexto es ese directorio.
Runtime vs build
ARG TELEGRAM_TOKEN no se embebe como ENV en el contenedor final, pero Docker desaconseja usarlo para credenciales: el valor sale en docker history y, si usas Buildx GitHub Actions en un repo público, en las attestations de provenance en modo max. ENV sí queda en la imagen. Ninguno de los dos es el sitio del token del bot.
Railway/Fly/Vercel env en el servicio, no en el Dockerfile. Preview: otro token (preview).
Para un token de build (npm privado, GIT_AUTH_TOKEN de un repo privado), el camino documentado es --secret + RUN --mount=type=secret,id=…. El mount vive en /run/secrets/<id> durante esa instrucción y no queda en el layer. Eso no sustituye el env de runtime del webhook: el bot no lee /run/secrets en cada mensaje.
docker image history --no-trunc imprime el CREATED BY completo. Ahí se ve el ARG/ENV. --format '{{.CreatedBy}}' si quieres greppearlo en CI.
Pin y USER no bastan
Digest ancla el blob. Si el blob tiene el .env, anclaste el leak. USER node no quita archivos del layer.
Tabla

| Cosa | ¿En imagen? | Dónde vive |
|---|---|---|
Código src/ | sí | COPY explícito |
| lockfile | sí | reproducible |
.env | no | runtime env |
.git | no | ignore |
| sqlite prod | no | volumen |
node_modules host | no | pnpm install en build |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
Token en docker history | ENV/ARG | env runtime |
| Imagen de 1 GB | COPY data/ | ignore + volumen |
| Build lento | contexto enorme | .dockerignore |
.env.example copiado como .env | glob mal | nombres explícitos |
| Leak en layer 4 aunque rm | COPY luego rm | nunca COPY el secret |
Relación con el resto
- Inyección: secretos.
- Blob anclado: digest.
- Uid: no-root.
- CI: GitHub Actions.
Checklist
-
.dockerignorecon.env*.git*.dbnode_modules - No
COPY . .si puedes listar paths - Cero
ARGde tokens -
docker historysin strings secretos - sqlite solo en volumen
- Ensayo:
docker run --rm img ls -a /appsin.env
FAQ
¿Multi-stage borra el secret del stage final? El stage de build sigue en el daemon local. No lo subas. Mejor no copiarlo nunca.
¿BuildKit secret mounts? Para npm tokens de build, no para el bot. El bot es runtime.
¿Vercel? No hay tu Dockerfile. Igual no subas .env al repo. .gitignore + dashboard.
Lista mínima
.env
.env.*
.git
node_modules
*.db
*.db-wal
*.db-shm
data/
secrets/
Añade lo que tu repo tenga (coverage/, .next/, dumps). El ignore del build no es el .gitignore: Git puede trackear src/ y Docker igual no debe mandar data/prod.db que alguien commiteó por error.
CI: un step git grep -n COPY -- Dockerfile que falle si hay COPY . .. Barato. El mismo job puede docker build y docker run --rm img test ! -f /app/.env.
Fly/Railway usan el Dockerfile del repo: el ignore tiene que estar commiteado, no solo en tu laptop. Preview que construye desde Git sin el archivo reintroduce el leak.
El ensayo: construye, docker history --no-trunc y busca sk- / bot. Vacío. ls en el contenedor sin .env.
Un ignore de 15 líneas evita un rotate de todos los tokens. Es el diff más barato del mes. Si ya publicaste una imagen con .env, el tag viejo sigue en el registry: bórralo, rota el token, no “sobrescribas latest” y asumas que el layer murió.
CI que construye desde un checkout sucio (archivos locales no ignoreados) es otro vector. El runner debe estar limpio o el ignore tiene que cubrir basura de OS (.DS_Store, Thumbs.db).
No esperes a que alguien docker pull tu imagen “privada” en un registry mal configurado. El layer ya viajó.
Siguiente paso: si el contexto ya es mínimo y igual hay leak, secretos y kill switch. Sin runtime: curso.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



