Hermes en tu Mac, expuesto al mundo en 2 minutos: el agente Go que controla Docker desde la nube
Despliega el modelo open-source Hermes en tu MacBook, con una URL HTTPS pública apuntando al container — sin sidecars, sin SSH, sin alquilar GPUs. Así construimos el agente Go que orquesta Docker desde un plano de control remoto.
Víctor Rojas
CEO, Aztecknology
Qué es Hermes y por qué te importa
Hermes es un modelo agentico open-source de Nous Research, expuesto vía un gateway compatible con la API de OpenAI. Bajas la imagen oficial (nousresearch/hermes-agent:v2026.4.16 — siempre anclada a CalVer, jamás latest), corres gateway run y tienes un endpoint en :8080 que cualquier SDK de OpenAI sabe consumir.
La promesa es enorme: inferencia soberana. Tu modelo, tu hardware, tus pesos cacheados en un volumen Docker. Sin facturar tokens a OpenAI ni a Anthropic, sin colocar tus prompts del producto en servidores ajenos.
¿La parte fea? Llevar eso de “corre en mi MacBook” a “está expuesto al mundo y sobrevive a un reboot”.
El problema: Docker + acceso público + ciclo de vida
Las opciones convencionales son tres, todas con costo:
- VPS con GPU. Funciona, pero te dejas $$$ al mes y la soberanía se diluye — dependes del proveedor, sus términos, su uptime.
- Tu propia máquina con Docker. Sin gastos extra, pero el container vive aislado. No es accesible desde Internet. Y nadie en tu equipo sabe qué está corriendo dónde.
- Despliegues manuales:
docker runen una terminal,ngrok http 8080en otra. Funciona hasta que cierras el laptop, hasta que ngrok recicla la URL, hasta que el token termina en un.bashrcque se filtra a un PR.
Lo que falta es una capa de control declarativa: “yo desde la nube te empujo el estado deseado, tú reconcilias localmente”. Eso no existe como producto comercial para Docker en una Mac. Así que lo construimos.
La arquitectura: un agente Go, dos abstracciones
Cuando montamos LexGuard en Aztecknology, este fue uno de los primeros componentes que decidimos hacer in-house. El resultado es un proceso Go de larga duración, single-binary (~20 MB), que corre en tu Mac y mantiene un WebSocket abierto contra el plano de control en la nube. Lo describimos internamente como:
Un cliente con estado que recibe un estado deseado desde un plano de control y reconcilia vía semántica mount/update/unmount. React, pero para DevOps.
Dos abstracciones sostienen todo:
- Runtime = el motor de containers (Docker hoy; en roadmap están podman, gvisor y firecracker). Es el dueño del ciclo de vida.
- Target = dónde se dispatchea una tarea (
hostejecuta víaos/execdirecto;dockerejecuta víadocker execdentro del container).
El stack interno:
- Un WebSocket de control (frames pequeños: handshake, heartbeat, estados de containers).
- Un WebSocket de datos separado (para streams pesados: MCP stdio, proxies HTTP/SSE).
- Un reconciliador
StateManagerque diffea por hash — mismo algoritmo conceptual que React. - Un tunneler ngrok dentro del propio binario — no hay un proceso
ngrokaparte ni un sidecar.
El último punto es el que más decisiones de diseño nos ahorró, y la sección 6 le dedica todo el espacio.
Instalar el agente
Comandos reales, probados. Requisitos: Go 1.25+ y Docker (opcional pero recomendado para este post).
git clone https://github.com/nei-digital/lexguard.git
cd lexguard/agent
# Build single-binary
go build -o agent ./cmd/agent
# Apunta al plano de control
./agent config set addr lexguard.app/api/infrastructure/agent/<TU_TOKEN>
./agent config show
# Boot
./agent --log-level=debug
Dónde quedan las cosas (por si quieres dormir tranquilo):
~/Library/Application Support/nei-digital/agent/settings.json— config del agente.~/Library/Application Support/nei-digital/agent/agent.log— todos los logs.~/Library/Application Support/nei-digital/agent/mcp-servers.json— whitelist de servidores MCP que puedes spawnnear (tema del próximo post de la serie).
Notarás que el token de ngrok nunca se persiste aquí. Eso es intencional. Más en la sección del tunnel.
El payload anotado de Hermes
Una vez que el agente está corriendo, el plano de control le empuja tareas como ésta:
{
"id": "hermes",
"image": "nousresearch/hermes-agent:v2026.4.16",
"cmd": ["gateway", "run"],
"ports": [{ "host": 8080, "container": 8080, "protocol": "tcp" }],
"volumes": [
{ "type": "volume", "source": "hermes-data", "target": "/opt/data" }
],
"restartPolicy": "unless-stopped",
"resources": {
"memoryBytes": 4294967296,
"nanoCpus": 2000000000
},
"expose": {
"ngrok": {
"containerPort": 8080,
"protocol": "http",
"authToken": "2ABcDef...",
"domain": "hermes.aztecknology.ngrok.app",
"basicAuth": "user:supersecret"
}
}
}
Campo por campo:
id: identificador estable. El container queda comolexguard-<id>-<hash>y se etiqueta conlexguard.task=<id>— para quedocker pssiga siendo legible.image: anclada a versión específica. Si tu compañero hacelatest, te enojas.restartPolicy: "unless-stopped": sobrevive a reboot del Mac. Hermes vuelve solo después de un crash.resources: 4 GB de RAM, 2 cores. Si lo omites, el container no tiene límite — peligroso en una Mac compartida con tu Slack y tu Chrome.expose.ngrok.authToken: se empuja por tarea, NO se guarda ensettings.json. El agente nunca persiste credenciales de tunnel. Cuando reinicies el proceso, no hay residuos.expose.ngrok.basicAuth: autenticación a nivel del edge de ngrok, antes de que el request llegue a tu Mac. Una línea, defensa en profundidad.expose.ngrok.domain: dominio reservado (feature de pago de ngrok). Si lo omites, recibes un*.ngrok-free.appque cambia cada vez.
El tunnel ngrok integrado (la parte que cambió todo)
Esta es la sección que más conversaciones nos ha generado con otros equipos que despliegan IA. La mayoría de tutoriales usan ngrok como CLI separado — abres una terminal, corres ngrok http 8080, esperas que no se caiga. Aquí el agente lo hace dentro de su propio binario usando la librería oficial golang.ngrok.com/ngrok. Cuatro beneficios concretos:
1. Cero sidecars. No hay un container extra ni un proceso ngrok corriendo aparte. El sistema operativo ve un solo binario. pgrep te devuelve un PID.
2. Lifecycle atado al container. Cuando el StateManager cancela el contexto de la tarea, el tunnel se cierra automáticamente. No hay PIDs huérfanos cuando matas un container.
3. Hot-swap del spec del tunnel sin reiniciar el container. El reconciliador diferencia dos hashes: HashTask(task) y HashExpose(task). Si solo cambia el tunnel (ej. quieres rotar el basicAuth o cambiar el domain), el agente mueve el forwarder pero deja el container vivo. Hermes no se reinicia, no pierdes el warm-up del modelo en memoria.
4. Soporta puerto dinámico. Si pones host: 0, el agente hace InspectHostPort en vivo cuando Docker asigna el puerto, y reescribe el spec del tunnel antes de abrirlo. Útil cuando tienes varios containers compitiendo por puertos.
Eventos que ves llegar al plano de control:
tunnel_readycon{ taskId, containerId, status, publicUrl }.tunnel_closedcuando se cancela.tunnel_errorsi el token está mal o el puerto no se publicó a tiempo.
Reconciliación: idempotencia por hash
El algoritmo del reconciliador es simple y deliberado:
- El agente calcula un
SHA-256determinístico del spec. - Mismo payload → mismo hash → no-op (
hash match, container intacto, el agente ni se mosquea). - Cualquier cambio (env, cmd, image, recursos) → hash distinto → stop + recreate.
- Mandar
{"tasks": []}es la forma documentada de decir “borra todo lo que gestionas”.
Esto es el mismo modelo mental que Kubernetes, pero en una sola Mac, en un solo binario. Sin etcd, sin control plane ajeno, sin kubelet. Veinte megabytes de Go.
Pruébalo: cURL contra tu Hermes público
Una vez que el plano de control emite tunnel_ready, hablas con tu Hermes desde cualquier parte del mundo:
curl https://hermes.aztecknology.ngrok.app/v1/chat/completions \
-u "user:supersecret" \
-H "Content-Type: application/json" \
-d '{
"model": "hermes",
"messages": [{"role":"user","content":"Hola, qué eres?"}]
}'
La API es compatible con OpenAI gracias al gateway run de Hermes. Eso significa que cualquier SDK de OpenAI funciona apuntando a esa URL — el de Python, el de Node, el de Go, el de Swift. Cambias OPENAI_BASE_URL y listo.
Operación: estados, crash-loop, hot-swap
Algunos detalles del día a día que te van a ahorrar madrugadas:
Crash-loop: si Hermes muere 5 veces en 60 segundos, el agente lo para y emite status crash_loop. El plano de control automáticamente flippea enabled=false, así que no se reanima en loop infinito quemando tu CPU. Los umbrales (crashLoopWindow, crashLoopThreshold) son configurables por tarea.
Disabled: enabled=false en el spec = container parado pero logs preservados. docker logs lexguard-hermes-<hash> sigue funcionando. Cuando lo vuelves a habilitar, arranca limpio pero con la historia disponible para debug.
Volúmenes: nunca se barren automáticamente. Tus pesos cacheados, tu estado de /opt/data, todo sobrevive a un docker rm. Esto es deliberado — los volúmenes son caros de recrear (pesos del modelo pueden ser GB).
Networks: las que crea el agente sí se barren cuando ya no hay task referenciándolas. No te deja basura en docker network ls.
Qué sigue
Este es el primer post de una serie de tres sobre cómo construimos la infraestructura agentica de LexGuard:
- Post 2 (próximamente): el mismo agente también puede spawnnear servidores MCP stdio y exponerlos sobre WebSocket — convirtiendo cualquier MCP local en algo accesible públicamente. Aplicaciones: dar a tu Claude/Cursor en la nube acceso a tu filesystem local, a tu Postgres local, a tus herramientas internas.
- Post 3: cómo conectar tus leads a IA que responde bien, usando MCP — el mismo plano de control orquestando agentes que gestionan WhatsApp, Instagram y Messenger.
- Post 4: cómo MCP le da memoria de marca a Claude, Gemini y GPT — la capa de coherencia de contenido encima de todo esto.
La idea grande
Despegar infraestructura agentica no requiere alquilar GPUs ni mudarte a Kubernetes. Requiere una capa de control declarativa, idempotente, y un binario pequeño que la ejecute donde tú decidas. Tu MacBook, tu servidor on-prem, una Linux box debajo del escritorio. El agente no le pide permiso al sistema operativo — solo reconcilia.
En Aztecknology construimos esto porque lo necesitábamos para LexGuard. Si te interesa montarlo en tu infra (Hermes, Llama, Mistral, lo que sea) con observabilidad y tunnels gestionados, hablemos. Esto es exactamente el tipo de problema que resolvemos.
¿Quieres el código? El agente vive en lexguard/agent. El spec wire-format de container está en agent/internal/interfaces/interface.container.go. El tunnel in-process en agent/internal/tunnels/ngrok.go. El reconciliador con hot-swap en agent/internal/routes/routes.containers.go. Para arquitectura completa, ver agent/PHASES.md y agent/HERMES.md.