Logotipo de Bitdoze

Ejecuta herramientas CLI de IA en Docker o Podman de forma segura (Guía 2026)

Ejecuta herramientas CLI de IA en Docker o Podman de forma segura: contenedores aislados para Claude Code, Amp, Gemini CLI y OpenCode, con configuraciones persistentes y cero riesgo para el host.

Dragos

Actualizado Publicado 85 min de lectura

Terminal ejecutando herramientas CLI de IA como Claude Code, Amp y Gemini CLI dentro de un contenedor Docker aislado

Los agentes de programación con IA pueden escribir archivos, ejecutar comandos de shell y leer todo lo que tu cuenta de usuario puede alcanzar. Esta guía te muestra cómo ejecutar herramientas CLI de IA en Docker o Podman dentro de un entorno seguro y aislado, de modo que un rm -rf alucinado o una instalación de dependencias fallida no puedan tocar tu máquina. Construirás una única imagen de contenedor que aloja Claude Code, Amp, Factory.ai, Gemini CLI, OpenCode y GitHub Copilot CLI, cada una con sus propias configuraciones, claves API y montajes de proyectos.

¿Por qué poner las herramientas CLI de IA en contenedores?

Instalas una CLI, te pide un comando de shell, quiere ejecutar una compilación, decide hacer npm install -g de algo. Cada uno de esos pasos ocurre con los permisos de tu usuario en tu máquina. Un contenedor convierte eso en un sandbox desechable: el agente obtiene un shell y tú puedes borrarlo todo con docker compose down cuando termines.

La versión honesta del argumento: un contenedor es aislamiento, no una máquina virtual. Comparte tu kernel. Si montas tus claves SSH o el socket de Docker dentro, le has entregado tu host al agente de todos modos. Todo en esta guía está construido en torno a no hacer eso.

Si quieres la misma idea pero con el agente viviendo en un servidor en lugar de tu portátil, nuestra guía de despliegue de NanoClaw cubre un agente Claude aislado en contenedor en un VPS.

¿Por qué ejecutar herramientas CLI de IA en Docker o Podman? (aislamiento y sandbox)

Lo único que realmente te rompe

No montes $HOME ni $HOME/.ssh en el contenedor del agente. Tampoco ~/.aws, ~/.config/gcloud o /var/run/docker.sock. Un agente con tu clave privada SSH y el socket de Docker es un agente con root en tu host y capacidad de llegar a todos los servidores a los que tú puedes llegar. Monta solo la carpeta del proyecto.

Ejecutar agentes de programación con IA en sandbox mediante contenedores te da seis cosas:

  • Aislamiento: los comandos de shell del agente se ejecutan contra el sistema de archivos del contenedor, no contra tus dotfiles
  • Reproducibilidad: tú y un compañero ejecutáis versiones de herramientas idénticas byte a byte
  • Seguridad: prueba un agente experimental sin que reescriba tu ~/.zshrc global
  • Múltiples configuraciones: contenedores separados para “clave API del trabajo” y “clave API personal”
  • Reversión limpia: docker compose down y el desastre desaparece
  • Fijado de versiones: fija Node y Python por proyecto en lugar de pelearte con nvm y pyenv

Lo que los contenedores no te dan:

  • Aislamiento de kernel. Una fuga del contenedor es un compromiso del host. Si necesitas una frontera de seguridad real, usa una VM (es la misma razón por la que ejecutamos código no confiable en VMs, no en contenedores).
  • Protección de los datos montados. El agente puede borrar cualquier cosa dentro de los directorios que montes. Monta un directorio de proyecto, no un directorio de proyectos que no puedas permitirte perder.
  • Protección frente a la red. El contenedor tiene salida a internet a menos que configures lo contrario. Un agente aún puede exfiltrar lo que pueda leer.

Requisitos previos: Docker Desktop o Podman Desktop

Elige un motor de contenedores. Docker si ya lo conoces, Podman si estás en Fedora o quieres rootless por defecto. Todo lo demás de esta guía funciona igual.

Docker

Docker Desktop (Mac/Windows) o Docker Engine (Linux):

Docker Engine 29 (noviembre de 2025) convirtió el almacén de imágenes containerd en el predeterminado en instalaciones nuevas. Docker Desktop es gratis para uso personal; los planes de pago aplican a empresas más grandes.

Verifica:

bash
docker --version
docker compose version
text
Docker version 29.7.2, build ...
Docker Compose version v2.x.x

Podman

Podman Desktop en Mac/Windows, paquetes nativos en Linux. Podman 6.0 (2026) eliminó el soporte de slirp4netns, CNI, iptables y cgroups v1. El modo rootful sigue funcionando bien: lo que cambió es que la red rootless ahora tiene exactamente una opción, pasta (varios Gbps en lugar de los antiguos ~570 Mbps a los que llegaba slirp4netns).

bash
# Fedora/RHEL/CentOS
sudo dnf install podman podman-compose

# Ubuntu/Debian
sudo apt install podman podman-compose

# macOS (via Homebrew)
brew install podman podman-compose

En macOS y Windows, inicializa la máquina de Podman:

bash
podman machine init
podman machine start

Verifica:

bash
podman --version
podman compose version
podman info --format '{{.Host.Security.Rootless}}'

El último comando debería imprimir true en una instalación normal de Linux.

Versiones en esta guía

Verificado en septiembre de 2026: Docker Engine 29.7.2, Podman 6.1.0, imagen base nikolaik/python-nodejs:python3.14-nodejs26-bookworm (Node 26.8.2, Python 3.14.7). Los instaladores de CLI cambian a menudo: si una URL devuelve 404, consulta la documentación actual de la herramienta en lugar de asumir que el comando está mal.

Arquitectura del contenedor: la imagen base con Python y Node

Una imagen, dos runtimes. Casi todas las herramientas CLI de IA son un paquete de Node o un script de shell que además quiere Python para sus utilidades, y reconstruir esa pila por herramienta es tiempo perdido.

Usamos nikolaik/python-nodejs, que incluye:

  • Node.js 26.8.2 con npm, además de Corepack para yarn y pnpm
  • Python 3.14.7 con pip, pipenv, poetry y uv
  • Un usuario no root pn con UID 1000 y GID 1000
  • Prompt Starship para un shell legible dentro del contenedor
  • Siembra automática de dotfiles en el primer arranque, para que un home vacío montado por bind siga funcionando

¿Por qué Node 26 en lugar de Node 25? Node 25 era una línea impar, no LTS, y llegó a su fin de vida a mediados de 2026. Si construiste este contenedor desde una guía de 2025, tu imagen base está en un runtime EOL sin parches de seguridad. Esa es toda la razón para reconstruir.

El detalle del UID 1000 importa más de lo que parece. Como pn es UID 1000, los archivos que crea dentro de un ~/websites montado por bind quedan en propiedad del UID 1000 en tu host, que es tu usuario normal de Linux en la mayoría de las distribuciones. Si tu usuario del host no es 1000, lee la sección de permisos en la solución de problemas.

Paso 1: crea la estructura del proyecto

Ejecutar el contenedor sin root

Todo lo que sigue se hace como tu usuario normal, incluido en Podman rootless, donde no interviene sudo en ningún punto del flujo de trabajo. Esa es la razón por la que aquí no aparecen los atajos del grupo docker ni el “simplemente ejecútalo como root”.

Docker

bash
mkdir -p ~/docker-ai-tools ~/dev-home ~/websites
cd ~/docker-ai-tools

Podman

bash
mkdir -p ~/podman-ai-tools ~/dev-home ~/websites
cd ~/podman-ai-tools

Para qué sirve cada directorio:

  • ~/docker-ai-tools o ~/podman-ai-tools: los archivos de configuración del contenedor (Dockerfile, archivo compose, .env)
  • ~/dev-home: el directorio home del usuario del contenedor, montado por bind. Guarda las configuraciones de las CLI, el estado de autenticación y las herramientas instaladas. Sobrevive a las reconstrucciones del contenedor.
  • ~/websites: tus proyectos reales, montados por bind en el contenedor. Es lo único que el agente debería poder tocar.

No hagas chmod 777 a estos directorios

Muchas guías antiguas de contenedores te dicen que ejecutes chmod 777 en los puntos de montaje para arreglar errores de permisos. No lo hagas. Hace que cada archivo de tu directorio de proyecto sea escribible por cualquiera en la máquina.

El usuario del contenedor es UID 1000 / GID 1000, así que en Linux simplemente iguala la propiedad:

bash
sudo chown -R 1000:1000 ~/dev-home ~/websites

Si tu usuario del host no es UID 1000, usa $(id -u):$(id -g) en lugar de los números literales y lee la sección de solución de problemas: te encontrarás con el desajuste de UID en la otra dirección.

En macOS y Windows, Docker Desktop y Podman Desktop se encargan del mapeo por ti. Ni chmod ni chown.

Paso 2: crea el Containerfile/Dockerfile

Cambio respecto a la versión de 2025

La etiqueta de la imagen base pasó de python3.14-nodejs25-bookworm a python3.14-nodejs26-bookworm. Node 25 está en fin de vida. Si copias una sola cosa de esta actualización, copia la línea FROM.

Docker

Crea ~/docker-ai-tools/Dockerfile:

dockerfile
# Dockerfile
# Base image with Node.js 26, Python 3.14, and package managers
FROM nikolaik/python-nodejs:python3.14-nodejs26-bookworm

SHELL ["/bin/bash", "-c"]

# The image already has a non-root user "pn" (UID 1000)
USER root

# Install system dependencies first
RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    jq \
    curl \
    vim \
    nano \
    htop \
    tree \
    ripgrep \
    rsync \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Install Starship prompt system-wide
# This works even when /home/pn is bind-mounted from the host
RUN curl -sS https://starship.rs/install.sh | sh -s -- -y \
 && mkdir -p /opt/skeleton/.config \
 && starship preset catppuccin-powerline -o /opt/skeleton/.config/starship.toml

# Minimal Bash configuration with Starship
RUN cat > /opt/skeleton/.bashrc <<'BRC'
# Bash initialized for pn user
# Enable Node Corepack and Yarn
corepack enable >/dev/null 2>&1 || true
corepack prepare yarn@stable --activate >/dev/null 2>&1 || true

# Quality of life alias
alias python=python3

# Initialize Starship prompt
if command -v starship >/dev/null 2>&1; then
  eval "$(starship init bash)"
fi
BRC

# Entrypoint script to seed dotfiles on first run
RUN cat > /usr/local/bin/boot.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail

# Seed ~/.bashrc if missing (e.g. empty bind-mounted /home/pn)
if [ ! -f "/home/pn/.bashrc" ]; then
  cp /opt/skeleton/.bashrc /home/pn/.bashrc
fi

# Seed Starship config if missing
mkdir -p /home/pn/.config
if [ ! -f "/home/pn/.config/starship.toml" ]; then
  cp /opt/skeleton/.config/starship.toml /home/pn/.config/starship.toml
fi

# Default to interactive bash if no command was provided
if [ $# -eq 0 ]; then
  set -- bash
fi
exec "$@"
SH
RUN chmod +x /usr/local/bin/boot.sh

# Switch back to the non-root user
USER pn
WORKDIR /home/pn/app

ENTRYPOINT ["/usr/local/bin/boot.sh"]
CMD ["bash"]

Podman

Crea ~/podman-ai-tools/Containerfile. El contenido es idéntico al Dockerfile anterior: Podman lee Dockerfiles sin problema, solo usa el nombre Containerfile por convención.

dockerfile
# Containerfile
# Base image with Node.js 26, Python 3.14, and package managers
FROM nikolaik/python-nodejs:python3.14-nodejs26-bookworm

SHELL ["/bin/bash", "-c"]

# The image already has a non-root user "pn" (UID 1000)
USER root

# Install system dependencies first
RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    jq \
    curl \
    vim \
    nano \
    htop \
    tree \
    ripgrep \
    rsync \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Install Starship prompt system-wide
# This works even when /home/pn is bind-mounted from the host
RUN curl -sS https://starship.rs/install.sh | sh -s -- -y \
 && mkdir -p /opt/skeleton/.config \
 && starship preset catppuccin-powerline -o /opt/skeleton/.config/starship.toml

# Minimal Bash configuration with Starship
RUN cat > /opt/skeleton/.bashrc <<'BRC'
# Bash initialized for pn user
# Enable Node Corepack and Yarn
corepack enable >/dev/null 2>&1 || true
corepack prepare yarn@stable --activate >/dev/null 2>&1 || true

# Quality of life alias
alias python=python3

# Initialize Starship prompt
if command -v starship >/dev/null 2>&1; then
  eval "$(starship init bash)"
fi
BRC

# Entrypoint script to seed dotfiles on first run
RUN cat > /usr/local/bin/boot.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail

# Seed ~/.bashrc if missing (e.g. empty bind-mounted /home/pn)
if [ ! -f "/home/pn/.bashrc" ]; then
  cp /opt/skeleton/.bashrc /home/pn/.bashrc
fi

# Seed Starship config if missing
mkdir -p /home/pn/.config
if [ ! -f "/home/pn/.config/starship.toml" ]; then
  cp /opt/skeleton/.config/starship.toml /home/pn/.config/starship.toml
fi

# Default to interactive bash if no command was provided
if [ $# -eq 0 ]; then
  set -- bash
fi
exec "$@"
SH
RUN chmod +x /usr/local/bin/boot.sh

# Switch back to the non-root user
USER pn
WORKDIR /home/pn/app

ENTRYPOINT ["/usr/local/bin/boot.sh"]
CMD ["bash"]

Ejecutar como usuario no root con USER pn

La línea USER pn del final es la relevante para la seguridad. Todo lo instalado en tiempo de ejecución, cada archivo que el agente crea y cada proceso que el agente lanza se ejecuta como UID 1000 dentro del contenedor, no como root. Los pasos de instalación siguen usando USER root temporalmente porque necesitas root para ejecutar apt-get, pero la imagen final y el contenedor en ejecución son no root.

Eso importa para el sandbox porque la mayoría de las fugas de contenedores dependen de root dentro del contenedor más un bug del kernel o una mala configuración (un socket montado, un flag privilegiado, un --cap-add). Ejecutar como UID 1000 elimina el camino fácil.

Si quieres el recorrido en profundidad de lo que los usuarios no root dentro de los contenedores te protegen y de lo que no, tenemos una guía aparte sobre ejecutar un contenedor como usuario no root.

Dos excepciones que conviene conocer:

  • Puertos por debajo de 1024. Un usuario no root no puede vincularlos dentro del contenedor. Los servidores de desarrollo que ejecutarías junto a un agente (3000, 4321, 5173, 8080) están todos por encima de 1024, así que rara vez molesta.
  • Instalación de paquetes del sistema. apt-get install necesita root. Por eso la sección “instalar un comando que falta” más abajo usa docker exec --user root.

Qué hace este Containerfile

  • Parte de nikolaik/python-nodejs: Node 26.8.2 y Python 3.14.7 ya configurados
  • Instala lo esencial y aburrido: git, jq, curl, vim, htop, tree, ripgrep, rsync, herramientas de compilación
  • Instala Starship: un prompt que muestra la rama de git y la versión de Node/Python, para que sepas en qué contenedor estás
  • Siembra configuraciones esqueleto: boot.sh copia dotfiles en un home vacío montado por bind en el primer arranque
  • Se ejecuta como no root: USER pn, UID 1000 / GID 1000
  • Mantiene tu home real fuera: nada en esta imagen referencia $HOME del host

Paso 3: crea la configuración de Compose

Docker

Crea ~/docker-ai-tools/docker-compose.yml:

yaml
services:
  ai-tools:
    build:
      context: .
      dockerfile: Dockerfile
    image: ai-tools:latest
    container_name: ai-tools
    restart: unless-stopped
    tty: true
    stdin_open: true
    environment:
      - TZ=Europe/Bucharest      # Change to your timezone
    env_file:
      - .env                     # API keys - do not commit this file
    volumes:
      - ${HOME}/dev-home:/home/pn
      - ${HOME}/websites:/home/pn/app/websites
    security_opt:
      - no-new-privileges:true
    # Uncomment only if a tool needs an OAuth callback or a local server
    # ports:
    #   - "127.0.0.1:3000:3000"  # bind to localhost, not 0.0.0.0

Podman

Crea ~/podman-ai-tools/podman-compose.yml:

yaml
services:
  ai-tools:
    build:
      context: .
      dockerfile: Containerfile
    image: ai-tools:latest
    container_name: ai-tools
    restart: unless-stopped
    tty: true
    stdin_open: true
    environment:
      - TZ=Europe/Bucharest      # Change to your timezone
    env_file:
      - .env                     # API keys - do not commit this file
    volumes:
      - ${HOME}/dev-home:/home/pn:z
      - ${HOME}/websites:/home/pn/app/websites:z
    security_opt:
      - no-new-privileges:true
    # Uncomment only if a tool needs an OAuth callback or a local server
    # ports:
    #   - "127.0.0.1:3000:3000"  # bind to localhost, not 0.0.0.0

Montaje de volúmenes explicado

  • ~/dev-home:/home/pn: configuraciones de usuario, estado de autenticación de las CLI, dotfiles. Persiste entre reconstrucciones.
  • ~/websites:/home/pn/app/websites: tus proyectos. Edítalos desde el host, el agente los edita desde dentro.
  • :z (Podman en Linux): reetiqueta el directorio para SELinux. Usa :z para directorios compartidos, :Z para los privados. Omítelo por completo fuera de Linux o si SELinux no está en modo enforcing.
  • No hace falta :rw: lectura-escritura es el valor por defecto. Solo escribes :ro cuando quieres quitar el acceso de escritura.

Valida el archivo antes de compilar. Compose ignora silenciosamente las erratas en algunos campos:

bash
docker compose config
# or: podman compose config

Si el YAML combinado se imprime y el servicio ai-tools tiene ambos volúmenes, todo bien. Si falta un volumen en la salida, tienes un problema de indentación.

Cuándo exponer puertos para callbacks de OAuth

La mayoría de las herramientas CLI de IA se autentican con una clave API en una variable de entorno o con un flujo de código de dispositivo (imprimen una URL, pegas un código). Ninguna de las dos necesita un puerto.

Añade un puerto solo cuando una herramienta abre un listener HTTP local para una redirección OAuth en localhost:

  • Una herramienta CLI requiere autenticación OAuth mediante un callback en localhost
  • Estás ejecutando un servidor de desarrollo local dentro del contenedor que quieres abrir en el navegador del host
  • Una herramienta incluye una interfaz web para iniciar sesión o configurar

Vincula el puerto publicado a 127.0.0.1, no a todas las interfaces. "3000:3000" publica en todas las interfaces que tiene el host, que en un VPS significa internet. "127.0.0.1:3000:3000" no.

Paso 4: compila e inicia el contenedor

Docker

bash
cd ~/docker-ai-tools

# Build the image
docker compose build

# Start the container detached
docker compose up -d

# Enter the container
docker exec -it ai-tools bash

Para detenerlo:

bash
docker compose down

Podman

bash
cd ~/podman-ai-tools

# Ensure the Podman machine is running (macOS/Windows only)
podman machine start

# Build the image
podman compose build

# Start the container detached
podman compose up -d

# Enter the container
podman exec -it ai-tools bash

Para detenerlo:

bash
podman compose down

Verifica que realmente está en ejecución:

bash
docker compose ps
text
NAME       IMAGE            COMMAND                  SERVICE   STATUS
ai-tools   ai-tools:latest  "/usr/local/bin/boot.…"  ai-tools  Up 12 seconds

Up es lo que quieres. Restarting en bucle significa que el entrypoint está fallando: consulta la solución de problemas.

La primera compilación tarda unos minutos (descarga una imagen base de ~580 MB). Las siguientes van por caché. Usa --no-cache solo cuando hayas cambiado de verdad la imagen base; tira todas las capas y vuelve a descargar todo.

Paso 5: verifica el entorno del contenedor

Dentro del contenedor, comprueba que las versiones son las que prometía la etiqueta:

bash
# You should not be root
whoami
id -u
# Expected: pn
# Expected: 1000

# Node and package managers
node -v            # v26.8.2
npm -v             # 11.x
yarn -v            # 4.x (via Corepack)

# Python and package managers
python3 -V         # Python 3.14.7
pip --version      # 25.x
poetry --version
uv --version

# Shell tooling
which starship
# Expected: /usr/local/bin/starship

# Mounts
ls -la ~/app/websites
touch ~/app/websites/.mount-test && ls -la ~/app/websites/.mount-test

Ese touch es la prueba que importa. Si falla, el montaje está mal y solo te enterarías más tarde, cuando el agente escriba un archivo en un directorio local del contenedor que desaparece en el próximo docker compose down.

Verifica también desde el host

bash
ls -la ~/websites/.mount-test

Si el archivo existe en tu host, el bind mount funciona en ambas direcciones. Bórralo y sigue adelante:

bash
rm ~/websites/.mount-test

Paso 6: instala las herramientas CLI de IA (Claude Code, Amp, Gemini CLI, OpenCode)

¿No sabes qué agente elegir? Comparamos las mejores herramientas y agentes de programación con IA en 2026. Instala más de una: pesan 10-100 MB cada una y no hay ninguna desventaja en tener Claude Code, Amp y OpenCode lado a lado.

A partir de aquí, todo se ejecuta dentro del contenedor, así que los comandos son idénticos para Docker y Podman. Entra una vez:

bash
docker exec -it ai-tools bash   # or: podman exec -it ai-tools bash

Si un instalador necesita root

Algunos instaladores escriben en /usr/local/bin y fallan como pn. Sal y vuelve a entrar como root, ejecuta solo esa instalación y regresa:

bash
# Docker
docker exec -it --user root ai-tools bash
# Podman
podman exec -it --user root ai-tools bash

Instala la herramienta, luego haz exit y vuelve a entrar como el usuario normal pn. Las herramientas instaladas como root en /usr/local/bin siguen en el PATH para pn, porque la lectura y ejecución están permitidas para todos ahí. Todo lo instalado en el home de root se pierde para ti, así que prefiere los scripts de instalación y rutas tipo sudo npm install -g en lugar de descargas manuales.

Instala Claude Code en el contenedor

El instalador nativo es el que debes usar. No le importa tu versión de Node y se actualiza solo.

bash
# Native installer (recommended)
curl -fsSL https://claude.ai/install.sh | bash

# Verify
claude --version

Si la configuración de Node de tu imagen base choca con el binario nativo, la vía npm sigue funcionando:

bash
npm install -g @anthropic-ai/claude-code
claude --version

Dónde acaban las cosas: el instalador nativo pone el binario en ~/.local/bin por defecto, así que sobrevive a una reconstrucción del contenedor solo porque ~/.local vive dentro del /home/pn montado por bind. Esa es la razón de que exista el montaje del home.

Instala Amp y Factory.ai

bash
# Amp
curl -fsSL https://ampcode.com/install.sh | bash

# Factory.ai CLI
curl -fsSL https://app.factory.ai/cli | sh

# Verify
amp --version
droid --version

Amp retiró su plan gratuito durante 2025, así que revisa los precios actuales antes de construir un flujo de trabajo alrededor. Contexto sobre cómo funciona en el editor: Amp Code, agente de programación con IA.

La CLI Droid de Factory es la otra que vale la pena instalar aquí, sobre todo si quieres apuntarla a tus propios endpoints de modelos en lugar de una suscripción empaquetada:

Factory Droid CLI

Puedes configurar Factory para que use tus propios proveedores (incluido un Ollama local): el ejemplo de archivo de configuración está más abajo.

Instala Gemini CLI, OpenCode y GitHub Copilot CLI

bash
# Gemini CLI
npm install -g @google/gemini-cli

# GitHub Copilot CLI (needs a Copilot subscription)
npm install -g @github/copilot

# OpenCode
curl -fsSL https://opencode.ai/install | bash

# Verify all three
gemini --version
copilot --version
opencode --version

Dos notas de la versión de 2025 de esta guía:

  • GitHub Copilot CLI cambió de paquete. El antiguo paquete beta @githubnext/github-copilot-cli está muerto. El actual es @github/copilot. Si ves una advertencia de obsolescencia, estás en el paquete viejo.
  • OpenCode se instala en ~/.opencode/bin en las versiones recientes, no en /usr/local/bin. Si te aparece opencode: command not found tras una instalación limpia, añade ~/.opencode/bin a tu PATH en el .bashrc sembrado.

¿Quieres saltarte la gestión de seis claves API? La suscripción OpenCode Go agrupa un montón de modelos detrás de una sola clave, que es la forma más barata de ejecutar un agente en contenedor sin una cartera llena de tokens:

OpenCode Go

Configuración completa, incluido el inicio de sesión en Zen y la configuración de proveedores: guía de configuración de OpenCode. Para los detalles de Copilot (modelos, límites del plan, el nivel de $10), consulta nuestra configuración de GitHub Copilot Pro CLI.

Y si prefieres enrutar Claude Code, Codex y Gemini CLI a través de un único endpoint con una clave y facturación por modelo, para eso sirve un agregador:

Agent Router

Autenticación con las herramientas CLI (OAuth y claves API)

bash
# Device-code or browser flows
claude          # then /login inside the session
amp login
droid           # then /login inside the session
opencode auth login
copilot         # then /login
gemini          # follow the auth prompt

OAuth dentro de un contenedor

El OAuth basado en navegador falla cuando la herramienta espera un callback en localhost y el puerto no es alcanzable desde el navegador de tu host. Tres soluciones, en orden de preferencia:

  1. Prefiere el flujo de código de dispositivo (imprime una URL y un código): no necesita ningún puerto.
  2. Copia la URL desde el terminal y pégala en el navegador del host. La mayoría de las herramientas lo aceptan.
  3. Publica el puerto del callback en 127.0.0.1 en el archivo compose, luego docker compose up -d para recrear el contenedor.

Si la herramienta insiste en un puerto, vincúlalo a loopback:

yaml
ports:
  - "127.0.0.1:3000:3000"

El estado de autenticación vive en ~/dev-home, así que te autenticas una vez por vida del token y lo mantienes entre reconstrucciones. Si borras ~/dev-home, tendrás que iniciar sesión de nuevo en todo.

Trabajar con proyectos

~/websites en tu host es /home/pn/app/websites dentro del contenedor. Eso significa:

  • Tú editas archivos en el host con Zed, VS Code, Cursor o vim
  • El agente edita los mismos archivos dentro del contenedor
  • Los cambios aparecen en ambos lados al instante (los bind mounts son en vivo, no copias sincronizadas)
  • Git funciona desde cualquiera de los dos lados sobre el mismo directorio .git

Flujo de trabajo de ejemplo: el host edita, el contenedor ejecuta

bash
# On the host
cd ~/websites
mkdir my-new-project && cd my-new-project
git init

# Inside the container
cd ~/app/websites/my-new-project
amp "Create a Next.js app with TypeScript"

Una trampa: no ejecutes el mismo gestor de paquetes en ambos lados del montaje. Si el host ejecuta npm install y el contenedor ejecuta pnpm install en el mismo directorio, se pelean por node_modules. Elige un lado como autoridad: dentro del contenedor es la opción sensata, porque las versiones del runtime están fijadas ahí.

Configurar las herramientas de IA dentro del contenedor

Las configuraciones persisten porque ~/dev-home se asigna a /home/pn. Edítalas desde cualquiera de los dos lados; una sesión de nano dentro del contenedor y code ~/dev-home/... en el host tocan el mismo archivo.

Ejemplo: modelos personalizados en Factory.ai

bash
# Inside the container
nano ~/.factory/config.json
json
{
  "custom_models": [
    {
      "model_display_name": "Claude Sonnet 4.5",
      "model": "claude-sonnet-4-5-20250929",
      "base_url": "https://api.anthropic.com",
      "api_key": "your-api-key-here",
      "provider": "anthropic",
      "max_tokens": 8192
    },
    {
      "model_display_name": "GPT-5 Codex",
      "model": "gpt-5-codex",
      "base_url": "https://api.openai.com/v1",
      "api_key": "your-openai-key-here",
      "provider": "openai",
      "max_tokens": 8192
    },
    {
      "model_display_name": "Qwen 3 (Local Ollama)",
      "model": "qwen3:14b",
      "base_url": "http://ollama:11434/v1",
      "api_key": "ollama",
      "provider": "generic-chat-completion-api",
      "max_tokens": 4096
    }
  ]
}

Fíjate en el base_url de Ollama: http://ollama:11434/v1, no http://localhost:11434/v1. Dentro de un contenedor, localhost es el propio contenedor. Si ejecutas Ollama como un servicio compose aparte (recomendado, ver abajo), el nombre del servicio es el hostname.

Tipos de proveedores de Factory.ai explicados

Tres tipos de proveedores:

  1. anthropic: modelos Claude vía la API de Anthropic

    • Base URL: https://api.anthropic.com
    • Usa la Messages API (v1/messages)
  2. openai: modelos GPT vía la API de OpenAI

    • Base URL: https://api.openai.com/v1
    • Usa la Responses API (obligatoria para GPT-5)
  3. generic-chat-completion-api: todo lo compatible con OpenAI

    • Funciona con OpenRouter, Fireworks, Together AI, Ollama, vLLM y la mayoría de los agregadores
    • Usa el formato OpenAI Chat Completions

Si un modelo devuelve 404, la causa habitual es haber elegido el tipo de proveedor equivocado para la forma de API de ese endpoint, no una clave mala.

Acceder a los archivos del host desde el contenedor

bash
# Inside the container
ls -la ~/.factory/     # Factory.ai configs
ls -la ~/.amp/         # Amp configs
ls -la ~/.config/      # everything else
cat ~/.bashrc          # shell config

Edita los mismos archivos desde el host:

bash
# On the host
code ~/dev-home/.factory/config.json
code ~/dev-home/.bashrc
code ~/dev-home/.config/starship.toml

Los cambios tienen efecto en el siguiente shell dentro del contenedor (o al instante, para los archivos de configuración que las herramientas releen en cada petición).

El problema de los archivos 0600

Los archivos de configuración que creas en el host como tu usuario son legibles por el UID 1000 dentro del contenedor, porque es el mismo UID en el caso común. Si tu UID del host no es 1000, un archivo con modo 0600 propiedad de, por ejemplo, UID 1002 será ilegible para pn. Arregla la propiedad cuando te lo encuentres:

bash
sudo chown 1000:1000 ~/dev-home/.factory/config.json

No lo arregles con chmod 666. Las claves API no pertenecen a archivos legibles por todos.

Configuración avanzada

Ejecutar modelos de IA locales con Ollama

La versión de 2025 de esta guía te decía que instalaras Ollama dentro del contenedor del agente. No lo hagas. Infla la imagen del agente y los pesos de los modelos acaban en un volumen que se borra al reconstruir. Ejecuta Ollama como su propio servicio y deja que el agente le hable por la red de compose.

Añade un segundo servicio a tu archivo compose:

yaml
services:
  ai-tools:
    # ... your existing config ...
    environment:
      - TZ=Europe/Bucharest
      - OLLAMA_HOST=http://ollama:11434
    depends_on:
      - ollama

  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    volumes:
      - ${HOME}/ollama:/root/.ollama
    # Only publish this if you want to reach it from the host too.
    # Don't publish it on a VPS.
    # ports:
    #   - "127.0.0.1:11434:11434"

Luego descarga un modelo y apunta una herramienta a él:

bash
docker compose up -d
docker exec -it ollama ollama pull qwen3:14b
docker exec -it ollama ollama list

# From inside ai-tools, confirm the name resolves
docker exec -it ai-tools curl -s http://ollama:11434/api/tags | jq '.models[].name'

Haz cuentas de RAM antes de empezar

Un modelo de 14B a Q4 necesita aproximadamente 9-10 GB de RAM para cargar y ejecutarse. Un modelo de 7B necesita unos 5 GB. En un VPS de 4 GB se quedará sin memoria (OOM) y se llevará el contenedor por delante. Pon un límite de memoria al servicio de Ollama para que no pueda hundir al host entero:

yaml
    deploy:
      resources:
        limits:
          memory: 12g

Si quieres la pila completa de Ollama con una interfaz web, nuestra guía de autoalojar Ollama con Docker Compose cubre Open WebUI, la gestión de modelos y la configuración de GPU.

Configuraciones con varios contenedores

Contenedores separados por contexto evitan que las claves API y los contextos se mezclen:

text
~/docker-ai-tools-personal/
  ├── docker-compose.yml     # container_name: ai-tools-personal
  ├── Dockerfile
  └── .env                   # personal keys

~/docker-ai-tools-work/
  ├── docker-compose.yml     # container_name: ai-tools-work
  ├── Dockerfile
  └── .env                   # work keys, separate ~/websites mount

Cambia el container_name y las rutas de montaje en cada archivo. Dos contenedores con el mismo container_name se negarán a arrancar: Compose falla con un conflicto de nombres, que al menos es un fallo claro.

Compartir contenedores con el equipo

bash
cd ~/docker-ai-tools
git init

Crea un .gitignore antes del primer commit:

bash
cat > .gitignore <<'EOF'
.env
dev-home/
websites/
EOF

git add Dockerfile docker-compose.yml .gitignore
git commit -m "Add AI tools container config"

Nunca hagas commit del archivo .env

El archivo .env contiene claves API vivas. Si hiciste commit de una por accidente, la solución es rotar las claves: quitar el archivo en un commit posterior no lo quita del historial. Añade .env al .gitignore antes del primer commit, no después.

Un compañero ejecuta entonces:

bash
git clone <your-repo>
cd <your-repo>
cp .env.example .env    # fill in their own keys
mkdir -p ~/dev-home ~/websites
docker compose up -d
docker exec -it ai-tools bash

El mismo Node, el mismo Python, las mismas herramientas instaladas. Esa es la parte de entorno de desarrollo reproducible de todo esto, y es la razón por la que hacer commit del Dockerfile vale esos cinco minutos.

Integración con el IDE

El contenedor es donde se ejecutan las herramientas. Tu editor se queda en el host, donde ya viven tu renderizado de fuentes, tus extensiones y tus atajos de teclado.

VS Code Dev Containers para agentes de IA

Dev Containers permite a VS Code ejecutar su servidor dentro del mismo contenedor que usa el agente, de modo que el terminal integrado, las tareas y el agente ven runtimes idénticos. Crea ~/docker-ai-tools/.devcontainer/devcontainer.json:

json
{
  "name": "ai-tools",
  "dockerComposeFile": ["../docker-compose.yml"],
  "service": "ai-tools",
  "workspaceFolder": "/home/pn/app/websites",
  "remoteUser": "pn",
  "shutdownAction": "stopCompose",
  "customizations": {
    "vscode": {
      "extensions": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
    }
  }
}

Pasos:

  1. Instala la extensión Dev Containers (el antiguo nombre “Remote - Containers” ya no existe).
  2. Abre la carpeta ~/docker-ai-tools en VS Code.
  3. Ejecuta Dev Containers: Reopen in Container.

Si VS Code se queja de que el contenedor ya existe, lo tienes en ejecución desde docker compose up -d. Ejecuta primero docker compose down: Dev Containers quiere ser dueño del ciclo de vida.

Para una alternativa basada en terminal en macOS orientada a flujos de trabajo con agentes, mira nuestra guía del terminal cmux para agentes de programación con IA. Y si prefieres tener la IA dentro del propio editor en lugar de en un shell de contenedor:

Windsurf

Edición directa de archivos desde el host

La opción más simple, y la que la mayoría de la gente acaba usando:

bash
# On the host
code ~/websites/my-project

# In the container, the agent sees the changes immediately
cd ~/app/websites/my-project
amp "Refactor this component"

Los bind mounts no sincronizan: son el mismo inodo. No hay paso de “esperar a que sincronice” ni posibilidad de una copia desfasada en un lado.

Solución de problemas

Permission denied en ~/dev-home o ~/websites

En Linux, la solución es casi siempre un desajuste de UID. Comprueba ambos lados:

bash
# On the host
id -u
ls -lan ~/websites | head -3

# In the container
docker exec -it ai-tools id

El usuario del contenedor pn es UID 1000. Si tu usuario del host también es 1000, simplemente arregla la propiedad una vez:

bash
sudo chown -R 1000:1000 ~/dev-home ~/websites

Si tu UID del host es distinto, o haces chown del montaje a 1000 como arriba, o cambia el user: del archivo compose a tu UID del host y define HOME:

yaml
    user: "1002:1002"
    environment:
      - HOME=/home/pn

En Linux con SELinux, añade la etiqueta al volumen (Podman lo necesita más a menudo que Docker):

bash
chcon -Rt container_file_t ~/dev-home ~/websites

O usa :z / :Z en la lista de volúmenes del compose en lugar de reetiquetar manualmente.

En macOS y Windows, esto es casi siempre un problema de rutas, no de permisos. Confirma que los directorios existen en el host y que no usaste ~ dentro del archivo compose: ~/websites en YAML no lo expande Compose, por eso los ejemplos usan ${HOME}/websites.

El contenedor no arranca o se reinicia constantemente

Mira los logs primero, siempre:

Docker

bash
docker compose logs --tail 50
docker logs ai-tools

Podman

bash
podman compose logs --tail 50
podman logs ai-tools

Causas habituales:

  • Puerto ya en uso: Error starting userland proxy: listen tcp4 0.0.0.0:3000: bind: address already in use. Encuentra al dueño con ss -ltnp | grep 3000 y deténlo o cambia el puerto publicado.
  • Fallo al descargar la imagen: normalmente una errata en la etiqueta de la imagen base o falta de red. Confirma que la etiqueta existe en la página de etiquetas de python-nodejs.
  • Fallo al montar el volumen: la ruta del host no existe. Compose crea los directorios que faltan cuando el origen es una ruta relativa, pero una ruta ${HOME}/... inexistente puede producir un error confuso. mkdir -p ~/dev-home ~/websites y reintenta.
  • Estado Exited (0): el entrypoint se ejecutó y bash salió porque no hay TTY. Asegúrate de que tty: true y stdin_open: true están ambos en el archivo compose.
La autenticación de la herramienta de IA falla
  1. Prueba el flujo de código de dispositivo antes de tocar puertos. Es la opción menos frágil.
  2. Copia la URL de autenticación al navegador del host y complétala ahí.
  3. Publica el puerto del callback en 127.0.0.1 y recrea el contenedor.
  4. Comprueba el reloj. Los flujos OAuth rechazan tokens cuando la hora del contenedor se desvía. date dentro del contenedor debería coincidir con el host; la variable TZ maneja la visualización, no el reloj en sí.

Ejemplo, Factory.ai:

bash
droid
# then /login; if it prints a URL, paste that URL into your host browser
Los cambios no se sincronizan entre host y contenedor

Confirma que los montajes realmente existen:

Docker

bash
docker inspect ai-tools --format '{{json .Mounts}}' | jq
docker inspect ai-tools --format '{{.HostConfig.Privileged}}'

Podman

bash
podman inspect ai-tools --format '{{json .Mounts}}' | jq

Forma esperada:

text
/home/you/dev-home  ->  /home/pn
/home/you/websites  ->  /home/pn/app/websites

Si el montaje está presente pero las escrituras no llegan a ninguna parte, comprueba si el agente se ejecutó desde una ruta local del contenedor. /home/pn/app fuera de /home/pn/app/websites no está montado: solo lo está el subdirectorio. Los archivos que el agente escribe en /home/pn/app/foo.txt desaparecen en el próximo docker compose down.

Privileged debe ser false. Si es true, quita privileged: true del archivo compose antes de hacer cualquier otra cosa.

El agente borró algo que no debería

Si borró archivos dentro de ~/websites, también desaparecieron del host: los bind mounts no son snapshots.

Lo que te salva:

  • Git. Haz commit antes de entregarle una tarea a un agente. git status después, git checkout -- . si salió mal.
  • Un backup que no esté en el mismo disco. Para ~/dev-home hablamos de configuraciones y estado de autenticación: molesto de perder, no catastrófico.
  • Montajes acotados. Si montas un solo directorio de proyecto en lugar de un directorio de doce, el radio de explosión es un proyecto.

Lo que no te salva: docker compose down. Elimina el contenedor, no los archivos del bind mount.

Instalar comandos que faltan en el contenedor

Las imágenes base son mínimas a propósito. Te encontrarás con command not found la primera vez que un agente quiera rg, ffmpeg o un cliente de Postgres:

bash
bash: rg: command not found
bash: psql: command not found

Dos formas de arreglarlo.

Opción 1: instalar temporalmente (solución rápida)

Docker

bash
# Enter as root
docker exec -it --user root ai-tools bash

# Always update the index first, or apt can't find the package
apt-get update
apt-get install -y ripgrep postgresql-client

exit
docker exec -it ai-tools bash

Podman

bash
# Enter as root
podman exec -it --user root ai-tools bash

apt-get update
apt-get install -y ripgrep postgresql-client

exit
podman exec -it ai-tools bash

Temporal significa temporal

Los paquetes instalados así viven en la capa escribible del contenedor y desaparecen en el próximo docker compose down o reconstrucción. Vale para “necesito psql ahora mismo”, no para algo que vayas a necesitar la semana que viene.

Olvidar el apt-get update previo es la causa más común de Unable to locate package. Ejecútalo siempre: un índice cacheado de una capa anterior suele estar desfasado.

Opción 2: actualiza el Dockerfile y reconstruye (permanente)

Añade el paquete a la línea apt-get install y reconstruye:

dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    jq \
    curl \
    vim \
    nano \
    htop \
    tree \
    ripgrep \
    rsync \
    postgresql-client \
    ffmpeg \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

Luego:

Docker

bash
cd ~/docker-ai-tools
docker compose build
docker compose up -d
docker exec -it ai-tools bash

# Verify
rg --version
psql --version

Podman

bash
cd ~/podman-ai-tools
podman compose build
podman compose up -d
podman exec -it ai-tools bash

# Verify
rg --version
psql --version

Qué sobrevive a una reconstrucción

  • Paquetes del sistema (git, rg, psql, ffmpeg): reconstruidos dentro de la imagen, ahí permanentemente
  • Herramientas CLI de IA: reinstaladas. Viven en el home montado por bind, así que la mayoría sobrevive si el instalador las puso en ~/.local/bin o ~/.opencode/bin, pero los globales de npm dentro de la imagen no
  • Autenticación: normalmente sobrevive en ~/dev-home; vuelve a ejecutar el comando de login si no
  • Configuraciones en ~/dev-home: se conservan
  • Proyectos en ~/websites: se conservan

Si una herramienta de IA muere tras una reconstrucción, simplemente vuelve a ejecutar su instalador. Son 30 segundos y es la razón de que los pasos de instalación sean de una línea.

Paquetes comunes que podrías necesitar

  • git: control de versiones, y la mayoría de los agentes escriben mensajes de commit
  • curl / wget: peticiones HTTP y descargas
  • jq: parseo de JSON, útil para respuestas de API
  • ripgrep (rg): búsqueda rápida; varios agentes lo invocan por shell
  • vim / nano: edición dentro del contenedor
  • htop: ve qué está comiendo memoria cuando una compilación se atasca
  • tree: vista general de directorios
  • rsync: sincronización de archivos
  • build-essential: toolchain de C/C++ para módulos nativos de npm
  • postgresql-client: psql para trabajo con bases de datos
  • ffmpeg: manejo de medios

Buenas prácticas de seguridad para agentes de IA en sandbox

Tu configuración de contenedor es un conjunto de capacidades. Todo lo de esta lista reduce lo que el agente puede hacer si algo sale mal.

  • Nunca hagas commit de claves API. Usa .env + env_file, o mejor, una configuración de secretos de Docker Compose
  • Rota las claves que hayas usado dentro del contenedor, sobre todo las que tocaron un contenedor compartido
  • Monta solo ~/websites y ~/dev-home. No $HOME, no ~/.ssh, no ~/.aws
  • Nunca montes /var/run/docker.sock. Es root en el host
  • Ejecuta como no root. Ya configurado con USER pn (UID 1000)
  • Mantén la imagen base actualizada. Reconstruye con --no-cache cuando cambie la etiqueta base
  • Vincula los puertos publicados a 127.0.0.1. No a 0.0.0.0
  • Define límites de recursos. Un agente desbocado debería ser lento, no fatal

Montajes de solo lectura y límites de recursos

Dos cambios de compose que recortan el radio de explosión sin romper nada:

yaml
services:
  ai-tools:
    # ... your existing config ...
    security_opt:
      - no-new-privileges:true   # blocks setuid escalation inside the container
    mem_limit: 8g                # kill the process instead of the host
    cpus: 4
    pids_limit: 512              # stops fork bombs from taking down the host

Verifica que los flags se aplicaron de verdad:

bash
docker inspect ai-tools \
  --format 'mem={{.HostConfig.Memory}} pids={{.HostConfig.PidsLimit}} sec={{.HostConfig.SecurityOpt}}'

Esperado: mem=8589934592, pids=512 y [no-new-privileges:true]. Si mem=0, el límite no se aplicó y el agente puede consumir toda la RAM del host.

Ahora la versión más fuerte, para cuando el agente solo necesita leer un árbol de referencia:

yaml
    volumes:
      - ${HOME}/dev-home:/home/pn
      - ${HOME}/websites:/home/pn/app/websites
      - ${HOME}/reference-docs:/home/pn/reference:ro   # read-only

docker exec -it ai-tools sh -c 'touch /home/pn/reference/x' debería fallar con Read-only file system. Esa es la prueba.

cap_drop: ALL no es gratis

Quitar todas las capacidades de Linux (cap_drop: [ALL]) es un paso real de hardening, y también rompe apt-get install dentro del contenedor en ejecución, porque root pierde CAP_CHOWN, CAP_SETUID y compañía. Elige una:

  • Portátil / uso de desarrollo: omite cap_drop. Quieres poder instalar paquetes sobre la marcha.
  • Contenedor de agente de larga vida: añade cap_drop: [ALL] y haz cada instalación de paquetes reconstruyendo la imagen.

El conjunto de capacidades por defecto de Docker ya está reducido; cap_drop: ALL es el último 10% de la ganancia y el primer 90% de la molestia.

Variables de entorno y claves API

Mantén las claves fuera de la imagen y fuera de git. Compose lee .env automáticamente para la sustitución de variables, y env_file inyecta los valores en el contenedor:

yaml
# docker-compose.yml or podman-compose.yml
services:
  ai-tools:
    env_file:
      - .env
    environment:
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - GEMINI_API_KEY=${GEMINI_API_KEY}
bash
# ~/.env - never commit this
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=...

Las variables de entorno son visibles para el proceso

Las variables de entorno se pueden leer desde /proc/<pid>/environ dentro del contenedor y aparecen en docker inspect. Eso está bien para un contenedor de un solo usuario en tu portátil. Para cualquier cosa multiusuario, usa secretos basados en archivos. Para el panorama completo de lo que funciona y lo que no:

Docker Compose Secrets

La otra dirección también importa: no pases todo tu entorno del host al contenedor. env_file: .env con cinco claves nombradas es deliberado. docker run -e $(env) es entregarle al agente tu token de sesión de AWS.

Si quieres la mecánica de ARG vs ENV vs la sustitución de Compose, nuestra guía sobre usar ARG y ENV para variables de entorno en Docker Compose cubre las diferencias y el orden de precedencia.

Limpieza

Eliminar contenedor e imágenes

Docker

bash
# Stop and remove the container
docker compose down

# Remove the built image
docker rmi ai-tools:latest

# Remove dangling images, stopped containers, and unused networks
docker system prune

# Also remove unused volumes - read the warning below first
docker system prune -a --volumes

Podman

bash
podman compose down
podman rmi ai-tools:latest
podman system prune

prune -a --volumes elimina volúmenes con nombre

docker system prune -a --volumes borra todas las imágenes y volúmenes sin uso de la máquina, no solo los de este proyecto. Si otro proyecto compose usa un volumen con nombre que no toca desde hace un mes, esos datos desaparecen. Ejecuta docker volume ls primero y lee la lista.

Los bind mounts (~/websites) nunca los toca prune: viven en el sistema de archivos del host, no en el directorio de datos de Docker.

Si el uso de disco de Docker sigue creciendo y prune no basta, el culpable suele ser el almacén de capas overlay2. Cubrimos recuperar espacio en disco de /var/lib/docker/overlay2 por separado.

Para una referencia rápida de los comandos que realmente escribirás, nuestra lista de comandos esenciales de Docker vale un marcador.

Empezar de cero

bash
docker compose down
docker rmi ai-tools:latest

# Optional: reset configs and auth state
# This deletes every CLI config and login in the container home
rm -rf ~/dev-home/*

# Optional: reset the image cache entirely
docker builder prune -af

# Rebuild
docker compose build --no-cache
docker compose up -d

Ese rm -rf ~/dev-home/* es el único paso irreversible de toda esta guía. Borra tus tokens de autenticación y las configuraciones de las herramientas. ~/websites queda intacto.

Docker vs Podman para herramientas de IA

El resumen honesto: para este caso de uso la diferencia es menor de lo que sugiere internet. Ambos ejecutan este mismo archivo compose, ambos soportan la misma imagen y ambos te dan el aislamiento que buscas.

Docker Podman
Arquitectura Cliente-servidor (daemon) Sin daemon, fork por comando
Privilegio por defecto El daemon corre como root; el modo rootless sin daemon requiere configuración Rootless por defecto
Uso de recursos en reposo ~50-100 MB para el daemon ~0 MB, nada persistente
Sintaxis de Compose docker compose podman compose
GUI de escritorio Docker Desktop Podman Desktop
SELinux Básico Nativo (:z / :Z)
Red rootless RootlessKit con slirp4netns por defecto (pasta soportado) Solo pasta en 6.x
Licencia Docker Desktop de pago para empresas grandes Gratis, proyecto CNCF Sandbox
Riesgo del socket de Docker El socket del daemon existe y es la vía clásica de escalada Sin daemon, sin socket

Elige Docker si: tu equipo ya lo usa, quieres el mayor ecosistema de ejemplos e integraciones de CI, o estás en macOS y quieres la experiencia de escritorio más suave.

Elige Podman si: estás en Fedora/RHEL, quieres contenedores rootless sin configuración extra, o te importa que no haya un daemon root siempre activo al que atacar.

Para un contenedor que ejecuta un agente con acceso a shell, el argumento de “sin proceso root persistente y sin socket de Docker” es el que realmente importa para la seguridad. El mecanismo es simple: si el agente encuentra /var/run/docker.sock dentro de su contenedor, puede levantar un contenedor privilegiado y ser root en tu host en unos tres comandos.

El desglose completo de arquitectura, licencias y rutas de migración está en nuestro artículo Podman vs Docker comparados.

Ejemplos de uso reales

Ejemplo 1: Factory.ai con modelos personalizados

bash
# Inside the container
cd ~/app/websites/my-app

droid

# In the Droid prompt, pick your model
/model
# Choose "Claude Sonnet 4.5" from custom models

"Add user authentication with Supabase, including sign-up, login, and protected routes"

El valor de la configuración de modelos personalizados: puedes apuntar Factory a un agregador barato para las ediciones rutinarias y a un modelo de frontera solo cuando lo necesitas, sin cambiar nada en el host.

Ejemplo 2: Amp con contexto de proyecto

bash
cd ~/app/websites/nextjs-blog

cat > AGENTS.md <<'EOF'
# Project: Next.js Blog

## Tech stack
- Next.js 15
- TypeScript
- Tailwind CSS
- MDX for blog posts

## Architecture
- App Router
- Server Components by default
- Client Components only when needed

## Commands
- Dev: npm run dev
- Build: npm run build
- Test: npm test
EOF

amp "Add a comments section using Supabase"

AGENTS.md es la ganancia de contexto más barata que existe. Es un archivo del proyecto, así que funciona para todos los agentes que lo lean, y funciona idéntico dentro del contenedor.

Ejemplo 3: varias herramientas de IA en secuencia

bash
# One agent implements
amp "Create a React component for a product card"

# Another reviews
claude "Review the ProductCard component and suggest performance improvements"

# A third writes tests
droid "Generate unit tests for ProductCard.tsx"

Distintos modelos son buenos en cosas distintas, y ejecutarlos contra el mismo checkout significa que cada uno ve el trabajo del anterior. También es una forma barata de contrastar la salida de un agente con una segunda opinión.

Si quieres el siguiente paso —dejar que un asistente dirija despliegues de Docker en un servidor— eso es un problema distinto con un radio de explosión mayor, y escribimos sobre dejar que un asistente de IA despliegue apps Docker por separado.

Buenas prácticas para herramientas CLI de IA en contenedores

  • Un contenedor por proyecto o contexto, no un contenedor para todo
  • Haz commit antes de entregar una tarea. Git es tu reversión, no el contenedor
  • Monta poco. Una carpeta de proyecto, no un directorio de proyectos
  • Añade un .dockerignore / .containerignore para que node_modules y .git no se envíen al contexto de compilación
  • Fija la etiqueta de la imagen base: python3.14-nodejs26-bookworm, nunca latest
  • Define límites de memoria y pids para que un agente desbocado degrade un servicio, no el host
  • Haz backup de ~/dev-home si reautenticarte en seis herramientas te molestaría
  • Reconstruye mensualmente para recoger los parches de seguridad de la imagen base
  • Mantén .env fuera de git y rota todo lo que se haya filtrado alguna vez

Preguntas frecuentes

¿Puedo ejecutar varias herramientas de IA simultáneamente?

Sí, y es la forma normal de usar esta configuración. Todas las CLI están instaladas en el mismo contenedor y comparten el proyecto montado:

bash
# Terminal 1
docker exec -it ai-tools bash
cd ~/app/websites/project1
amp "Implement feature X"

# Terminal 2, same container
docker exec -it ai-tools bash
cd ~/app/websites/project2
claude "Review the auth code"

Cada exec es una sesión independiente. Lo único que hay que evitar es que dos agentes editen el mismo archivo del mismo proyecto a la vez: se pisarán entre ellos, y eso es un problema de git que tendrás que desenredar a mano.

¿Los contenedores ralentizarán mis herramientas de IA?

No hay una ralentización medible para trabajo de CLI, con una excepción.

  • La E/S de archivos sobre bind mounts es casi nativa en Linux. En macOS y Windows, Docker Desktop y Podman Desktop pasan por una VM, y las instalaciones pesadas de node_modules son notablemente más lentas ahí. Mantén node_modules dentro del sistema de archivos del contenedor en lugar de en un bind mount si eso te duele.
  • La CPU y la memoria se comparten con el host en Linux: sin impuesto de VM.
  • Las peticiones de red a las APIs de IA salen directamente. La latencia la domina el modelo, no el contenedor.

El factor de rendimiento real es el modelo y tu red, no el motor de contenedores.

¿Es esto tan seguro como una VM?

No, y quien te diga lo contrario te está vendiendo algo.

Un contenedor comparte el kernel del host. Los exploits de kernel, las capacidades mal configuradas y los flags privilegiados son vectores de fuga del contenedor. Lo que esta configuración te da es protección contra el modo de fallo realista: un agente que ejecuta rm -rf, instala un paquete global roto o reescribe la configuración de tu shell. Es una frontera muy buena para accidentes y una frontera aceptable para código malicioso.

Si necesitas ejecutar código genuinamente no confiable, usa una VM (o una microVM). Si necesitas que un agente trabaje en tus propios proyectos, un contenedor no root, con capacidades limitadas y montajes acotados es el punto correcto de coste/beneficio.

¿Cuánta RAM y disco usa esto?

Para el contenedor del agente por sí solo: aproximadamente 1-2 GB de RAM en reposo con unas cuantas CLI instaladas, y 3-5 GB de disco para la imagen base más las cachés de node_modules. Funciona bien en un VPS de 2 GB si no estás ejecutando también modelos locales.

Añade Ollama y las cuentas cambian por completo: un modelo de 7B a Q4 son ~5 GB, un modelo de 14B son ~9-10 GB, antes de contar la caché KV. No ejecutes modelos locales en nada con menos de 16 GB de RAM si quieres que sean utilizables.

¿Necesito actualizar el contenedor cuando una CLI de IA se actualiza?

No. Las reconstrucciones son solo para las versiones del SO base, Node y Python.

bash
# Inside the container
claude update
amp update
droid update

# npm-installed tools
npm update -g @google/gemini-cli
npm update -g @github/copilot

Todo lo instalado en ~/dev-home (que es donde los instaladores nativos ponen sus binarios y donde los autoactualizadores escriben) persiste entre reinicios del contenedor. Un docker compose down seguido de up no lo pierde.

¿Puedo usar esta configuración en CI/CD?

Sí, y encaja mejor que un contenedor de agente de larga vida porque el runner ya es efímero. El patrón: compila la imagen, ejecuta el agente con el repositorio montado, captura el diff.

yaml
# .github/workflows/ai-review.yml
name: AI Code Review
on: [pull_request]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build AI tools image
        run: docker compose build
      - name: Run AI review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          docker compose up -d
          docker exec ai-tools claude -p "Review this PR for code quality and list any bug risks"

Dos cosas con las que tener cuidado: no des a los agentes de CI credenciales de escritura en tu repo (deben revisar, no hacer push), y recuerda que todo lo que el agente produce es salida no confiable: trátala como un comentario en el PR, no como una puerta que pueda bloquear merges.

¿Y si necesito una versión distinta de Node.js o Python?

Cambia la etiqueta de la imagen base. Ambos runtimes vienen de una sola imagen, así que eliges una combinación:

dockerfile
# Python 3.14 with Node 26 (what this guide uses)
FROM nikolaik/python-nodejs:python3.14-nodejs26-bookworm

# Python 3.13 with Node 26
FROM nikolaik/python-nodejs:python3.13-nodejs26-bookworm

# Slim variant - smaller, no build toolchain
FROM nikolaik/python-nodejs:python3.14-nodejs26-slim

Las combinaciones disponibles están en la página de etiquetas de nikolaik/python-nodejs. Fija la etiqueta completa y reconstruye; no uses latest, o tu entorno “reproducible” deja de serlo en un momento arbitrario.

Ese mismo truco también es práctico: si no tienes motivo para ejecutar Python 3.14 y quieres una imagen mucho más pequeña, python3.14-nodejs26-slim más los paquetes que realmente necesitas es varios cientos de MB más liviana.

¿Dónde debería ejecutar el contenedor del agente si lo quiero en un servidor?

Cualquier VPS con 2 GB de RAM es suficiente para el contenedor del agente por sí solo. Esta es la configuración que la gente usa para “un agente en un servidor al que puedo llegar desde cualquier parte”: el contenedor corre junto a tus otros servicios, mantiene un checkout de git y te conectas con docker exec.

Una máquina de €4-5/mes es de sobra para esto. Más allá de eso estás pagando por inferencia de modelos locales, no por el contenedor.

Hetzner Cloud

Los precios de los VPS subieron en general en 2026: si estás reconsiderando una máquina alquilada, mira qué cambió y cuándo gana un mini PC.

Dos reglas si te vas a remoto: nunca publiques los puertos del contenedor a internet y nunca montes el socket de Docker del host dentro. Un contenedor de agente remoto con el socket de Docker montado es un shell de root remoto para cualquiera que pueda escribirle un prompt.

Conclusión

Poner las herramientas CLI de IA en contenedores te da un sandbox en el que puedes permitirte ser descuidado. No una frontera de seguridad contra un atacante decidido, sino una frontera contra el modo de fallo que realmente ocurre: un agente que ejecuta un comando que no debería.

Con lo que acabas:

  • Aislamiento: los comandos de shell del agente se ejecutan contra el contenedor, no contra tus dotfiles
  • Persistencia: las configuraciones y el estado de autenticación viven en ~/dev-home, los proyectos en ~/websites
  • Reproducibilidad: el mismo Dockerfile te da a ti y a un compañero runtimes idénticos
  • Reversión: docker compose down y una reconstrucción te dejan un borrón y cuenta nueva
  • Múltiples configuraciones: un contenedor por contexto, con claves y montajes separados
  • Un host en paz: tu Node, Python y paquetes globales de npm quedan intactos

Las tres cosas que vale la pena hacer diferente respecto a la versión de 2025 de esta guía: pasar a python3.14-nodejs26-bookworm como imagen base, dejar de instalar Ollama dentro del contenedor del agente, y no montar por bind nada que no le entregarías en un prompt de shell.

Si estás construyendo el resto de la pila, las continuaciones que más importan son Podman vs Docker comparados si aún eliges motor, la guía de configuración de OpenCode o Amp Code, agente de programación con IA para la parte de herramientas, y las mejores herramientas y agentes de programación con IA en 2026 si prefieres comparar antes de comprometerte.

¿Listo para construirlo?

  1. mkdir -p ~/docker-ai-tools ~/dev-home ~/websites
  2. Añade el Dockerfile y el archivo compose de los pasos 2 y 3
  3. docker compose up -d && docker exec -it ai-tools bash
  4. Instala el agente que quieras y autentícate
  5. Cierra la tapa, vete, y sabiendo que el peor caso es una reconstrucción