Guía10 min

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 · 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.

EnfoqueDónde viven las herramientasReproducibleAislamiento
Instalación global en el host/usr/local, ~/.npm-globalNoNinguno
Version manager por proyecto.nvmrc, .python-versionParcialPoco
Dev container.devcontainer/devcontainer.json + DockerfileSí, commit a commitContenedor 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. El remoteUser que 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:

PropiedadFunciónCuándo
containerEnvVariables para todo el contenedor, estáticas hasta rebuildVersión de runtime, config global
remoteEnvVariables para el cliente/tool, actualizables sin rebuildPATH extendido, flags del agente
forwardPortsPuertos del contenedor visibles en localhostServidor de la app, debugger
remoteUser / containerUserUsuario con el que corre el tool / todo el contenedorCero root, permisos de bind mount
featuresPaquetes self-contained del ecosistema devcontainersgh CLI, docker-in-docker, aws-cli
postCreateCommandÚltima etapa de setup, con secretos de usuario disponiblespnpm install, git lfs pull

Los commands de ciclo de vida se ejecutan en orden: onCreateCommandupdateContentCommandpostCreateCommandpostStartCommandpostAttachCommand. 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.

Mapa de configuración donde el devcontainer.json enruta imagen, features, puertos y usuario del entorno

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:

  1. El agente lee el secreto de su propio gestor (1Password CLI, gh secret, variable del CI, keyring del host).
  2. Lo inyecta al contenedor como variable del host y lo referencia con ${localEnv:OPENAI_API_KEY}.
  3. 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/* traen node, vscode, etc.), hereda con remoteUser y evita containerUser a menos que el Dockerfile lo exija.
  • Si construyes tu Dockerfile, termina con USER no-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.

Etapas del ciclo de vida de un dev container: crear, arrancar, conectar y ejecutar el agente como usuario no-root

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.json versionado y sin secretos literales
  • image con tag de versión fija (nada de :latest) o Dockerfile con FROM fija
  • remoteUser no-root configurado o heredado de la imagen
  • postCreateCommand idempotente (volver a correrlo no rompe nada)
  • forwardPorts solo para lo que la app expone; el resto silencioso con portsAttributes
  • 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.