Guía10 min

Cómo desplegar un agente de IA con Docker paso a paso

Resumen

Guía para desplegar un agente de IA con Docker en un VPS: Dockerfile multi-stage para Node con imagen final pequeña, claves por entorno en runtime (nunca en la imagen), HEALTHCHECK y restart policy para auto-recuperación, logs que no llenan el disco y exposición del webhook. Con tabla de decisión pm2 vs Docker y checklist de cierre.

Docker
Contenedores apilados corriendo un agente con puertos publicados hacia un canal de mensajería

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.

Elegir dónde corre el agente es media decisión. La otra mitad es cómo se empaqueta para que corra igual en tu laptop y en un VPS de 5 USD, sin instalar dependencias a mano ni heredar el estado de la máquina. Docker resuelve exactamente eso: el contenedor lleva su propia versión de Node, sus dependencias compiladas y tu código; el host solo necesita el runtime. Esta guía arma el paquete mínimo que un agente en producción necesita: imagen multi-stage pequeña, claves fuera de la imagen, reinicio automático tras un fallo, chequeo de salud y logs que no llenen el disco.

Si aún no decidiste dónde desplegar, empieza por la comparativa de Vercel, Cloudflare Workers o VPS: los límites de tiempo de un serverless empujan al VPS + Docker cuando el agente hace trabajo largo o mantiene estado en disco. Y si el gasto por token te preocupa, revisa cuánto cuesta correr un agente: mover el hosting a un VPS plano cambia poco ese número, el costo real está en la API del modelo.

Por qué Docker y no un proceso con pm2

pm2 corre tu proceso directamente en el host. Si otro proyecto del mismo VPS actualiza dependencias, compila un paquete nativo o sube la versión de Node, tu agente hereda el daño. Con Docker cada proyecto lleva su propio Node y sus dependencias aisladas. Las ventajas concretas:

Criteriopm2 (proceso en host)Docker (contenedor)
Versiones de NodeUna compartida por proyectoUna por imagen, congelada
Dependencias nativasCompilan contra el hostCompilan una vez en el build
Mismo entorno en staging y prodDifícil de garantizarLa imagen es idéntica
RollbackReinstalar dependencias viejasCorrer el tag anterior de la imagen
Límites de memoria/CPUFrágiles (reinicio del proceso)Nativos (--memory, --cpus)
Curva de aprendizajeBajaMedia: Dockerfile, build, volúmenes

El costo de Docker es una capa más de abstracción y aprender tres o cuatro comandos. Para un agente que debe correr semanas sin que lo toques, el intercambio vale la pena.

Capas de aislamiento de un agente en contenedor: proceso, dependencias y red separados del host

Requisitos

  • Un VPS con Docker Engine instalado (Ubuntu 22.04/24.04 o Debian 12). La instalación oficial está en docs.docker.com, sección Engine, y son dos comandos. Si publicarás un webhook necesitas acceso SSH y los puertos 80/443 libres.
  • El código del agente: un proyecto Node/TypeScript con scripts build y start en el package.json. El patrón sirve igual para Python cambiando la imagen base.
  • Las claves del agente (API key del modelo, token del bot, URL de base de datos) a mano para pasarlas en runtime, nunca dentro de la imagen.

Paso 1 — Dockerfile multi-stage

Un build multi-stage usa una imagen "builder" con todo el toolchain (dev dependencies, compiladores) y una imagen final que solo copia lo que corre. Docker Docs lo documenta como el estándar para imágenes ligeras. Para un agente el beneficio es directo: la imagen final no arrastra dev dependencies ni cachés, arranca más rápido, descarga más rápido si algún día la subes a un registry y expone menos superficie ante un atacante.

Crea un Dockerfile en la raíz del agente:

# ── Etapa 1: builder (todo el toolchain) ──────────────
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ── Etapa 2: runtime (solo lo que corre) ──────────────
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
# El usuario no-root 'node' ya existe en la imagen oficial
USER node
COPY --from=builder --chown=node:node /app/package*.json ./
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

Qué hace cada decisión clave:

  • node:22-alpine como base: alpine es la variante mínima (~50 MB base vs ~200 MB de la completa). Fija la versión mayor (22): no uses latest, porque un rebuild meses después puede traer una versión incompatible.
  • npm ci en vez de npm install: exige lockfile y produce instalaciones reproducibles. Si tu repo usa pnpm, cambia la base a una imagen con corepack habilitado y usa pnpm install --frozen-lockfile; el patrón multi-stage es idéntico.
  • USER node: la guía de best practices oficial de nodejs/docker-node recomienda correr como non-root. Si el contenedor se compromete, el atacante no es root dentro del contenedor.
  • CMD ["node", "dist/index.js"] en forma exec (JSON array), no CMD npm start: la forma exec hace que node sea el proceso 1 del contenedor y reciba las señales del sistema (SIGTERM en docker stop). Con npm start el PID 1 es npm, que no siempre reenvía señales: el stop se vuelve lento o forzado.
  • COPY --from=builder de node_modules: necesario si el agente usa dependencias en runtime (Express, SDK del modelo). Si compilaste un bundle único (esbuild/tsup) basta copiar dist y el package.json.

Guarda también un .dockerignore junto al Dockerfile, o el COPY . . arrastrará basura al build:

node_modules
dist
.git
.env
.env.*
*.log

La línea de .env es crítica: si el COPY . . la lleva a la imagen, cualquier persona con acceso al registry tiene tus claves.

Paso 2 — Construir y correr en el VPS

En tu máquina local (o en CI):

docker build -t agente-ia:1.0 .
docker run --rm agente-ia:1.0 node -e "console.log('build ok')"

Luego transfiere la imagen al VPS. Dos caminos válidos: subir la imagen a un registry (Docker Hub, GHCR) y hacer docker pull en el servidor — el estándar cuando hay más de un servidor o CI — o construir directamente en el VPS con git pull && docker build. Para un primer despliegue, construir en el VPS es lo más simple.

En el servidor:

git pull
docker build -t agente-ia:1.0 .
docker run -d \
  --name agente \
  --restart unless-stopped \
  -p 3000:3000 \
  --env-file /srv/agente/agente.env \
  --memory 512m --cpus 0.5 \
  agente-ia:1.0

Qué significa cada flag:

  • -d: corre en background (detached).
  • --restart unless-stopped: si el contenedor se cae o el VPS se reinicia, Docker lo levanta de nuevo. Es el "auto-heal" básico; manualmente equivale a lo que hace systemd con un servicio, pero acá va incluido en el runtime del contenedor.
  • -p 3000:3000: publica el puerto del contenedor en el host. Docker Docs documenta que solo los puertos publicados son accesibles desde fuera; todo lo demás queda aislado. Si el webhook entra por HTTPS en 443 vía Nginx o Caddy en el host, ese proxy se conecta a localhost:3000 y puedes omitir el -p y usar una red Docker compartida — menos superficie expuesta.
  • --env-file: inyecta las claves en runtime, como variables de entorno dentro del contenedor.
  • --memory y --cpus: cotas duras. Un agente con un loop desbocado se detiene en lugar de tumbar el resto del VPS.

Paso 3 — Claves en runtime, nunca en la imagen

El .env del proyecto no va dentro del contenedor. Las claves entran como variables de entorno en docker run, y eso tiene tres ventajas:

  1. Puedes rotar la API key sin reconstruir la imagen: docker stopdocker run con el env-file nuevo.
  2. La imagen es la misma en staging y producción; solo cambia el env-file.
  3. docker inspect del contenedor muestra las variables solo a quien tenga acceso root en el VPS, no a quien descargue la imagen.

Crea /srv/agente/agente.env en el servidor (fuera del repo):

OPENAI_API_KEY=sk-...
TELEGRAM_BOT_TOKEN=123:ABC...
DATABASE_URL=postgres://...
PORT=3000

Y protégelo:

chmod 600 /srv/agente/agente.env

NODE_ENV=production conviene ponerlo en el Dockerfile (ya lo hicimos) porque afecta el build del código, no solo el runtime. Las demás claves son secretos por entorno: solo al runtime.

Paso 4 — HEALTHCHECK y auto-recuperación

Un contenedor "corriendo" puede estar muerto por dentro: el proceso vive pero se colgó esperando una respuesta que nunca llega. La instrucción HEALTHCHECK del Dockerfile le dice a Docker cómo distinguir vivo de zombie:

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

Para que funcione, el agente necesita un endpoint mínimo:

// dist ya compilado: en el fuente del servidor Express
app.get("/health", (_req, res) => res.json({ ok: true }));

El estado visible con docker ps pasa de Up 3 hours a Up 3 hours (healthy) — o (unhealthy) tras 3 fallos consecutivos. Con esa señal:

  • Monitoreo externo: UptimeRobot o similar apuntando a un endpoint de salud público, o un cron en el VPS que consulte docker inspect --format='{{.State.Health.Status}}' agente.
  • Reacción automática: --restart cubre proceso muerto; para el caso zombie necesitas algo que ejecute docker restart al ver (unhealthy). Un cron de una línea basta para empezar. La versión completa (autoheal container, orquestador) tiene sentido con varios servicios.

El chequeo de salud es el que convierte "creo que está vivo" en un dato verificable. Sin esto, el primer aviso de caída es un usuario quejándose en el canal.

Paso 5 — Logs sin llenar el disco

docker logs acumula todo lo que el proceso imprime. Un agente verboso (cada mensaje del usuario, cada llamada al modelo) puede generar gigabytes en semanas y llenar el disco del VPS. La solución es rotación nativa del logging driver:

docker run -d \
  --name agente \
  --restart unless-stopped \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  ...

Eso acota los logs a 30 MB por contenedor, rotando en 3 archivos. Como alternativa permanente, fija el default en /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Después de editar daemon.json: sudo systemctl restart docker (ojo: reinicia los contenedores con --restart automáticamente).

Comandos de operación diaria:

Panel de operación de un contenedor en vivo: salud, memoria y registros rotando

docker logs -f --tail 100 agente   # seguir en vivo los últimos 100 líneas
docker stats agente                 # CPU/RAM en vivo
docker restart agente               # tras cambiar env-file
docker ps -a                        # estado + health de todos

Checklist de cierre

Antes de dar el despliegue por terminado:

  • docker build termina sin warnings de credenciales y la imagen final < 300 MB.
  • El contenedor corre como non-root (docker exec agente whoaminode).
  • Ningún .env dentro de la imagen (docker run --rm agente-ia:1.0 ls -la /app | grep env no muestra nada).
  • docker inspect agente --format '{{.HostConfig.RestartPolicy.Name}}'unless-stopped.
  • curl localhost:3000/health{"ok":true} y docker ps muestra (healthy).
  • Los logs tienen rotación configurada (30 MB tope).
  • El webhook del canal (WhatsApp/Telegram/Slack) apunta a https://tu-dominio/webhook y responde 200.
  • Rollback probado una vez: docker run con el tag anterior funciona.

FAQ

¿Necesito Kubernetes o Docker Compose? No para un agente. Compose ayuda cuando tienes agente + base de datos + proxy en el mismo host y quieres describirlos en un solo archivo YAML; es un siguiente paso natural pero no un requisito. Kubernetes resuelve problemas de escala que un solo VPS no tiene todavía.

¿Puedo usar el mismo contenedor para varios agentes? Mejor no: un contenedor por agente da aislamiento de fallos, límites de recursos independientes y rollback por agente. Compose orquesta varios contenedores en el mismo host sin dolor.

¿Dónde vive el estado (conversaciones, colas)? Fuera del contenedor: un volumen Docker (-v agente-data:/app/data) para archivos locales o la base de datos del paso anterior. Si destruyes el contenedor, el volumen sobrevive; lo que está dentro del filesystem del contenedor no.

¿Cómo actualizo el agente sin downtime? Construye la imagen nueva con otro tag, corre el contenedor nuevo en otro puerto, verifica health, y cambia el proxy al puerto nuevo. Con un solo VPS y un webhook, un docker stop && docker run de 10 segundos también es aceptable para empezar.

El empaquetado en contenedor es la mitad mecánica de tener el agente en producción. La otra mitad — detectar fallos, medir costos y responder cuando algo se rompe — la cubre la guía de observabilidad para agentes.