Dev containers para coding agents: entorno reproducible con .devcontainer
Un dev container (devcontainer.json + Dockerfile) convierte el entorno de un coding agent en configuración versionada: mismas versiones de runtime, mismas features, mismo usuario no-root para todos. Cómo elegir entre image y build.dockerfile, cuándo usar el dev container universal de GitHub, qué va en containerEnv/remoteEnv y qué nunca (secretos), y el flujo local con Dev Container CLI. Checklist para que el agente monte su entorno sin sorpresas.
Por Marco Lee · Reportar un error
Un dev container es una imagen Docker configurada para desarrollo, y el devcontainer.json es el archivo que la declara: runtime, features, puertos, usuario y comandos de arranque. Cuando un coding agent opera dentro de un dev container, no trabaja "en tu máquina": trabaja en un entorno reproducible que cualquier colega (o el CI) puede reconstruir con los mismos resultados. Dejas de depender de lo que alguien instaló una vez en su laptop.
Esta guía es la pieza de entorno reproducible del flujo que ya cubrimos en worktrees y despliegue Docker: primero aíslas el trabajo, ahora aíslas las herramientas.
Qué resuelve un dev container
El problema real no es "instalar Node": es que cada entorno acaba distinto. El developer A tiene Node 20, el B tiene Node 22 con nvm, el CI corre la imagen de Node 22 slim y el coding agent de Claude Code, Codex o Cursor ejecuta otra versión de bash con otros bins globales. Un bug que no se reproduce localmente nace muchas veces de esa divergencia.
| Enfoque | Dónde viven las herramientas | Reproducible | Aislamiento |
|---|---|---|---|
| Instalación global en el host | /usr/local, ~/.npm-global | No | Ninguno |
| Version manager por proyecto | .nvmrc, .python-version | Parcial | Poco |
| Dev container | .devcontainer/devcontainer.json + Dockerfile | Sí, commit a commit | Contenedor propio |
El contrato es simple: existe .devcontainer/devcontainer.json, y cualquier herramienta que soporte el estándar (VS Code, Codespaces, la Dev Container CLI) puede crear el entorno a partir de él. Para un coding agent, ese archivo es la especificación del entorno: no hay que describir en un README qué instalar; se lee la configuración.
image vs. build.dockerfile: elige el punto de entrada
devcontainer.json tiene dos formas principales de declarar el contenedor:
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-22": usas una imagen ya publicada. Rápida, sin capa de build propia. ElremoteUserque trae la imagen se hereda (spec, remoteUser)."build": { "dockerfile": "Dockerfile" }: construyes tu imagen con un Dockerfile, normalmente co-ubicado en.devcontainer/. Necesario cuando el proyecto tiene su propio conjunto de herramientas (un tiempo de compilación, un SDK privado, un binario que no existe como feature).
La decisión práctica: arranca con una imagen oficial de devcontainers (devcontainers/images) y escala a Dockerfile cuando la imagen base no cubra el stack. Si no declaras ni image ni build, GitHub Codespaces usa el default dev container: una imagen universal con Node, Python, Java, Go, Rust, PHP, .NET, JupyterLab, Git, GitHub CLI, yarn y más — buena para empezar, mala para reproducibilidad fina porque la lista crece sola (GitHub Docs).
{
"name": "agente-node",
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-22",
"forwardPorts": [3000],
"customizations": {
"vscode": { "extensions": ["streetsidesoftware.code-spell-checker"] }
}
}
Qué configura un coding agent en devcontainer.json
No copies un devcontainer.json ajeno: la especificación (containers.dev JSON reference) define qué es estándar y qué es opinión de la herramienta. Las propiedades que importan para un agente:
| Propiedad | Función | Cuándo |
|---|---|---|
containerEnv | Variables para todo el contenedor, estáticas hasta rebuild | Versión de runtime, config global |
remoteEnv | Variables para el cliente/tool, actualizables sin rebuild | PATH extendido, flags del agente |
forwardPorts | Puertos del contenedor visibles en localhost | Servidor de la app, debugger |
remoteUser / containerUser | Usuario con el que corre el tool / todo el contenedor | Cero root, permisos de bind mount |
features | Paquetes self-contained del ecosistema devcontainers | gh CLI, docker-in-docker, aws-cli |
postCreateCommand | Última etapa de setup, con secretos de usuario disponibles | pnpm install, git lfs pull |
Los commands de ciclo de vida se ejecutan en orden: onCreateCommand → updateContentCommand → postCreateCommand → postStartCommand → postAttachCommand. La regla de oro para un agente: si un paso falla, los siguientes no corren — el spec lo define así (lifecycle scripts). Usa el formato array ["pnpm", "install"] para evitar el shell cuando no lo necesites, y el objeto de comandos paralelos solo cuando dos tareas no comparten estado.
Piensa el devcontainer.json como customization, no personalización: linters y runtimes sí, temas de UI no (GitHub Docs). Un coding agent que despliega en el entorno de otro respeta esa frontera: no instala preferencias personales en un entorno compartido.

Secretos fuera del contenedor: la línea que no se cruza
El devcontainer.json se versiona y se comparte. Por eso nunca pongas un API key, token o password literal en containerEnv o en build.args del Dockerfile: el archivo va al repo, cualquier persona con acceso al repo lo lee, y cualquier imagen construida queda con el secreto en capas históricas. Es el mismo principio que aplicamos a secretos en variables de producción y al .dockerignore: el archivo de configuración no es un vault.
El flujo correcto para un coding agent:
- El agente lee el secreto de su propio gestor (1Password CLI, gh secret, variable del CI, keyring del host).
- Lo inyecta al contenedor como variable del host y lo referencia con
${localEnv:OPENAI_API_KEY}. - El contenedor lo recibe solo en runtime, no en la imagen.
{
"containerEnv": {
"OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}"
}
}
Si ${localEnv:...} queda vacío porque la variable no existe en el host, el spec la deja en blanco sin avisar: agrega un postCreateCommand que falle temprano si el entorno no trae lo esperado (test -n "$OPENAI_API_KEY" || exit 1). Un agente que arranca con un secreto vacío no falla bonito: falla confuso.
Usuario no-root: el contenedor no es el host
La referencia del spec explica la distinción entre containerUser (el usuario con el que corre todo el contenedor) y remoteUser (el usuario con el que se conectan las tools, terminales, tasks y debugging). El default de ambos es root si la imagen no declara USER: un coding agent corriendo como root dentro del contenedor no es la misma superficie de riesgo que corriendo fuera, pero sí amplifica cualquier error de permisos (escribes donde no debes, borras lo que no revisas).
La práctica recomendada para agentes:
- Si la imagen base trae un usuario (las oficiales
devcontainers/*traennode,vscode, etc.), hereda conremoteUsery evitacontainerUsera menos que el Dockerfile lo exija. - Si construyes tu Dockerfile, termina con
USERno-root y solo da permisos donde hace falta. - Para problemas de bind mounts (los archivos montados del host pertenecen a tu UID local), deja
updateRemoteUserUID: true(default) para que el UID del contenedor se ajuste al tuyo (spec).
{
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22",
"remoteUser": "node",
"updateRemoteUserUID": true
}
El patrón "cero root" en dev containers es la misma mentalidad que pin de imágenes: no es paranoia, es eliminar la clase de error entera.
Flujo local: Dev Container CLI
No necesitas VS Code para usar un dev container. La Dev Container CLI (@devcontainers/cli en npm) crea y arranca el contenedor desde el devcontainer.json en CI, en un runner, o en el setup de un agente local:
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . pnpm test
Eso convierte el devcontainer.json en el único lugar donde vive el entorno: el coding agent ejecuta pnpm test dentro del contenedor aunque esté en una máquina nueva. Con gh codespace create el mismo archivo arranca un entorno en la nube — el flujo de Codespaces de GitHub es el mismo estándar, solo que el host es la nube.

Los features —unidades de instalación del ecosistema devcontainers (catálogo oficial)— son el atajo para no escribir cadenas largas de apt-get: ghcr.io/devcontainers/features/github-cli, docker-in-docker, aws-cli... una línea en features y el contenedor trae la herramienta exacta.
Checklist antes de commitear un devcontainer
-
.devcontainer/devcontainer.jsonversionado y sin secretos literales -
imagecon tag de versión fija (nada de:latest) o Dockerfile conFROMfija -
remoteUserno-root configurado o heredado de la imagen -
postCreateCommandidempotente (volver a correrlo no rompe nada) -
forwardPortssolo para lo que la app expone; el resto silencioso conportsAttributes - Variables host referenciadas con
${localEnv:...}y check temprano de presencia - Prueba mínima:
devcontainer up --workspace-folder .en una máquina limpia
FAQ
¿Dev container es lo mismo que Docker Compose? No. El devcontainer.json puede referenciar un dockerComposeFile para escenarios multi-servicio, pero su foco es describir el entorno de desarrollo; Compose describe la aplicación. Cuando solo necesitas la app corriendo, usa Compose; cuando necesitas herramientas y runtime para desarrollarla, dev container.
¿Features sustituyen al Dockerfile? En muchos casos sí: si tu stack son runtime + CLI + gh, un image oficial + features cubre el 90% sin mantener un Dockerfile. Construyes Dockerfile cuando necesitas capas propias (dependencias nativas, bins privados).
¿El dev container garantiza el mismo resultado en CI y local? Cierra la brecha de entorno, no la de determinismo: mismas versiones de runtime y herramientas, pero el código, datos y red del host siguen fluyendo según cómo montes el workspace. Es reproducible a nivel de herramienta, no a nivel de sistema completo; para el paso siguiente, mira self-hosting con Docker.
¿Un coding agent debe crear el dev container solo? Puede, pero el primer build de una imagen dev container es lento y necesita Docker disponible. El patrón práctico: el humano (o el CI) construye y sube la imagen, el agente trabaja dentro con devcontainer exec — así el agente no negocia con el daemon de Docker en cada arranque.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



