Self-hosting de un agente IA con Docker en un VPS: guía paso a paso
Resumen
Tutorial de self-hosting para agentes de IA: Docker y Compose en un VPS Ubuntu, Dockerfile Node.js, red interna, Caddy con HTTPS automático, actualizaciones sin downtime, checklist de endurecimiento y cuándo conviene quedarse en una plataforma en vez de auto-hospedar el agente.

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.
Ya comparaste Vercel, Cloudflare Workers y VPS y elegiste VPS: control total, costo plano y ninguna plataforma cerrándote una función de la noche a la mañana. El precio de ese control es que ahora la infraestructura eres tú: instalación de Docker, red entre contenedores, HTTPS, actualizaciones y endurecimiento. Esta guía es el camino mínimo que funciona, con las prácticas que la documentación oficial de Docker recomienda para producción. Las fuentes oficiales fueron consultadas el 3 de septiembre de 2026.
Antes de empezar: esta pieza cubre el cómo del self-hosting. Si tu decisión aún está abierta, la comparativa de plataformas te sirve de entrada, y la guía de secretos cubre cómo inyectar las credenciales del agente sin filtrarlas — los dos pasos se tocan aquí, pero cada guía profundiza lo suyo.
Requisitos
- Un VPS Ubuntu 22.04/24.04 con 1–2 GB de RAM (un agente Node.js consume poco; el pico es el build).
- Un dominio con registro A hacia la IP del VPS.
- Acceso SSH y 30 minutos.
Instalación de Docker Engine en Ubuntu: sigue la página oficial de instalación (repositorio apt de Docker, no el paquete docker.io de Ubuntu, que va desactualizado).
Paso 1 — Estructura del proyecto
mi-agente/
Dockerfile
compose.yaml
.env # fuera de git
src/
El Dockerfile para un agente Node.js sigue el patrón de la guía oficial de contenerización de Node:
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY src ./src
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app /app
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
El snippet usa node:22-alpine para leerse. En prod, FROM va con digest (node:22-alpine@sha256:…): si omites tag y digest, el builder asume latest. La guía de pin por digest cubre el imagetools inspect y por qué un tag semver sigue siendo un puntero. Lo mismo para image: caddy:2 más abajo.
Build multi-etapa: la imagen final no lleva pnpm ni cachés de instalación. USER node evita correr como root — la primera línea de defensa que pide la guía de seguridad de Docker Engine.
Paso 2 — Docker Compose: agente + reverse proxy
Docker Compose define los servicios del stack en un solo archivo. Aquí el agente y Caddy como reverse proxy con HTTPS automático:
services:
agente:
build: .
restart: unless-stopped
env_file: .env
expose:
- "3000"
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
volumes:
caddy_data:
Dos decisiones importantes:
exposeen el agente,portssolo en Caddy. El puerto del agente queda dentro de la red interna de Compose; lo único publicado hacia internet son 80/443. Docker advierte que un puerto publicado escucha en todas las interfaces por defecto — publica lo mínimo.- El volumen
caddy_datapersiste los certificados TLS. Sin volumen, Caddy re-emite certificados en cada recreación del contenedor y puede chocar con rate limits de Let's Encrypt.
Caddyfile (un dominio, HTTPS automático):
agente.tudominio.com {
reverse_proxy agente:3000
}
Con eso, Caddy provee y renueva el certificado TLS sin configuración extra.
Paso 3 — Desplegar y verificar
docker compose up -d --build
docker compose logs -f agente
curl -s https://agente.tudominio.com/health
Criterio de éxito: curl responde 200 y los logs muestran el agente escuchando en el puerto 3000 dentro de su red. Si curl falla pero docker compose ps muestra ambos contenedores arriba, el problema casi siempre es DNS (el registro A no apunta aún) o el firewall del VPS (abre 80/443, nunca 3000 hacia internet).
Paso 4 — Actualizar sin drama

git pull
docker compose up -d --build
docker image prune -f
up -d --build recrea solo los contenedores cuya imagen cambió. Con restart: unless-stopped, el agente vuelve solo tras un reinicio del VPS. Downtime real: los segundos del recreate — aceptable para un agente interno o con cola.
Si el agente atiende webhooks de Telegram/WhatsApp en vivo, agrega healthcheck al servicio y un segundo contenedor durante el switch (blue-green manual), o acepta la ventana de 2–5 segundos. Para la mayoría de agentes de un solo inquilino, la ventana corta es el trade correcto.
Paso 5 — Endurecimiento mínimo

- Rootless o usuario no-root en el Dockerfile (
USER node). La guía de seguridad de Docker Engine documenta las superficies de ataque del daemon y por qué el aislamiento por contenedor no es mágica. - Firewall del VPS: solo 22, 80, 443 abiertos.
ufw allow 80,443/tcpy nada más. - SSH por llave, contraseña deshabilitada.
- Secretos en
.envfuera de git, con rotación planificada. En Swarm existedocker secret; en un VPS de un solo nodo, el.envcon permisos 600 y backups cifrados es el trade razonable. - Backups del volumen de Caddy y de cualquier estado del agente (sqlite, memoria):
docker run --rm -v caddy_data:/data -v $PWD:/backup alpine tar czf /backup/caddy-$(date +%F).tgz /dataen un cron semanal. - Monitorea los tres síntomas: contenedor reiniciando en loop (
docker compose ps), disco lleno por logs o imágenes viejas (df -h,docker system prune), y certificado por expirar (Caddy lo renueva solo, pero verificacurl -vIde vez en cuando).
Errores comunes
| Síntoma | Causa típica | Fix |
|---|---|---|
curl da timeout | Firewall del VPS cerrado o DNS sin propagar | Abre 80/443; verifica dig +short dominio |
| 502 Bad Gateway | El agente escucha en localhost en vez de 0.0.0.0 | Bind del server a 0.0.0.0:3000 |
| Certificado no emitido | Volumen de Caddy sin persistir o dominio mal apuntado | Monta caddy_data, corrige el registro A |
| Agente muere tras horas (OOM) | RAM del VPS insuficiente para build + runtime | Build fuera del VPS o swap + límite de memoria en Compose |
| Webhooks llegan duplicados | Recreación del contenedor + reintento del proveedor | Healthcheck antes de switch + idempotencia en el handler |
Cuándo NO auto-hospedar
- Si el agente tiene picos impredecibles de tráfico: las plataformas serverless escalan a cero y tú no.
- Si no puedes pagar 30 minutos de mantenimiento mensual: el VPS exige parches y rotación; las plataformas lo hacen por ti.
- Si necesitas SSE/webhooks con timeouts larguísimos: revisa primero si la comparativa de plataformas resuelve con Workers o Functions, porque en VPS lo resuelves tú con configuración del proxy.
Siguiente paso: combina esta guía con sandboxing y permisos si el agente ejecuta código, y con la de secretos para el manejo de credenciales. Tu stack queda: Docker Compose + Caddy + secretos rotados + sandbox si aplica. Si todavía no tienes el runtime, el curso gratuito deja un agente local para meterlo después en el Dockerfile.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



