SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-21

Cómo alojar Moli, un navegador headless para agentes

Instala Moli en un VPS pequeño, expón CDP en loopback y conecta Playwright o tu agente. Revisa sus límites frente a Chrome, Canvas y multimedia.

Un navegador headless para un VPS pequeño

Moli es un navegador headless para agentes de IA. Es lo bastante pequeño para alojarlo en un VPS donde no cabe headless Chrome. Es un motor de navegador escrito en Rust, no un wrapper de Chromium, y responde al Chrome DevTools Protocol (CDP), el protocolo que ya utiliza su biblioteca de automatización. Instale un único binario, ejecute moli serve y dirija Playwright o el código de su propio agente a http://127.0.0.1:9222.

Lea las limitaciones antes de instalar nada. El proyecto indica claramente su alcance: no incluye un navegador con GUI, un compositor GPU, paridad píxel a píxel con Chrome ni compatibilidad de alta fidelidad con Canvas o la reproducción multimedia. Las páginas que necesiten esas funciones fallarán. Chrome real mediante Playwright sigue siendo la alternativa de respaldo. La última sección explica cómo decidir qué páginas lo necesitan.

Todos los comandos siguientes proceden del README del proyecto y de sus archivos de skills publicados, revisados en agosto de 2026. Todas las cifras de los gráficos son datos que el proyecto publicó sobre su propio motor, no mediciones realizadas en este sitio. Cada pie de gráfico lo indica. Si todavía está eligiendo un motor, el análisis más amplio de navegadores headless para agentes en un VPS cubre las alternativas.

¿Por qué Chrome sin interfaz gráfica usa tanta memoria?

Chrome es un navegador multiproceso. Cada pestaña y cada iframe de otro sitio obtiene su propio proceso de renderizado, y cada renderizador tiene su propio heap de V8 y sus propios búferes gráficos. Este diseño es adecuado para un equipo de escritorio, donde una pestaña bloqueada no debe cerrar toda la ventana. En un VPS de 2 GB, una sola operación de navegación puede consumir más memoria que la aplicación que realmente está ejecutando.

El proyecto rastreó 192 URL públicas variadas con cuatro motores y publicó el resultado.

ChartMixed public web crawl, 192 URLs, figures published by the Moli project
The data behind this chart
[
  {
    "engine": "Moli",
    "useful_pages": 103,
    "median_rss_mib": 73
  },
  {
    "engine": "Chrome Headless",
    "useful_pages": 101,
    "median_rss_mib": 773
  },
  {
    "engine": "Lightpanda",
    "useful_pages": 85,
    "median_rss_mib": 40
  },
  {
    "engine": "Obscura",
    "useful_pages": 57,
    "median_rss_mib": 39
  }
]

Chrome Headless devolvió 101 páginas útiles y Moli devolvió 103, por lo que, en esa muestra, ambos motores analizaron aproximadamente la misma proporción de la web. La diferencia está en la memoria: una RSS mediana (resident set size, la memoria que un proceso mantiene realmente en la RAM) de 773 MiB para Chrome frente a 73 MiB para Moli. La tendencia es creíble porque se deriva de la arquitectura de procesos. No debe asumir que la proporción exacta será la misma en sus páginas.

La mediana no es lo que provoca el problema. El pico sí. Cuando un equipo de 2 GB se queda sin memoria, el kernel selecciona un proceso y lo termina, y el registro queda en dmesg -T o journalctl -k:

Out of memory: Killed process 4211 (chrome) total-vm:2318936kB, anon-rss:1418324kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:3540kB oom_score_adj:0

Su agente nunca ve esa línea. Ve un navegador que dejó de responder, normalmente como un error de Playwright como page.goto: Page crashed o un destino cerrado. Ese error no menciona la memoria, por lo que el killer OOM (out of memory) es lo primero que debe comprobar cuando un agente falla de forma aleatoria en un equipo pequeño. Dimensionar el sistema para el pico equivale a elegir la RAM y la CPU para un VPS de agente.

Instale el binario de Moli con una versión fijada

El proyecto publica un instalador de shell y archivos tar precompilados en las versiones de GitHub. En agosto de 2026, la versión actual es 1.0.1, publicada el 18 de agosto de 2026. Las cifras de referencia citadas en esta guía fueron medidas por el proyecto con la versión 0.1.1. Considérelas una indicación aproximada del comportamiento del motor, no una garantía sobre la compilación que instale.

Fije la versión. Un instalador que siempre resuelve latest cambia el motor del navegador de su agente en la siguiente reconstrucción. Un cambio en el comportamiento del navegador debe programarse, no descubrirse por sorpresa.

El instalador de shell es la forma más rápida de empezar. Conviene leerlo antes de ejecutarlo.

curl --proto '=https' --tlsv1.2 -fsSL \
  -o /tmp/moli-installer.sh \
  https://github.com/lexmount/moli/releases/download/v1.0.1/moli-installer.sh
less /tmp/moli-installer.sh
sh /tmp/moli-installer.sh

Lea el script antes de ejecutarlo. Es corto. Selecciona un archivo del uname -m y después extrae un único binario en ~/.local/bin. En x86_64 usa moli-x86_64-unknown-linux-gnu.tar.gz y, en un servidor Arm, usa el archivo aarch64. Por tanto, tanto los planes VPS Arm como los x86 están cubiertos. Defina MOLI_INSTALL_DIR para instalarlo en otra ubicación. Observe cómo resuelve la versión: usa la versión más reciente, no el tag desde el que obtuvo el script. Esto está bien para una primera prueba, pero no para una reconstrucción reproducible.

Para cualquier instalación permanente, haga manualmente lo que hace el instalador y especifique usted mismo el archivo exacto. Así también puede colocar el binario en una ubicación a la que acceda un servicio del sistema. Además, evita enviar un script descargado directamente a un shell mediante una tubería.

cd /tmp
curl --proto '=https' --tlsv1.2 -fsSLO \
  https://github.com/lexmount/moli/releases/download/v1.0.1/moli-x86_64-unknown-linux-gnu.tar.gz
mkdir -p moli-pkg
tar -xzf moli-x86_64-unknown-linux-gnu.tar.gz -C moli-pkg --strip-components=1
sudo install -m 0755 moli-pkg/moli /usr/local/bin/moli
moli --version

moli --version mostrar la versión que fijó es toda la comprobación necesaria. moli: command not found inmediatamente después del instalador indica que el directorio de instalación no está en su PATH. El instalador muestra una línea con el directorio que debe añadir.

Extracción puntual con moli fetch

Muchos trabajos que un agente asigna a un navegador consisten en «carga esta URL y dime qué dice». Para eso no hace falta ningún servidor. moli fetch inicia el motor, carga una página, escribe un artefacto en la salida estándar y termina, por lo que no conserva memoria entre llamadas.

moli fetch --dump markdown --wait-until networkidle https://example.com
moli fetch --dump semantic_tree_text --wait-selector "main" https://example.com
moli fetch --dump json --wait-until networkidle https://example.com > page.json

El primero imprime la página como Markdown y comienza con # Example Domain. Markdown es el formato más económico para entregar a un modelo porque elimina el marcado y conserva el texto. semantic_tree_text mantiene los roles y la estructura. Es lo que necesita en una página con mucha navegación, donde los enlaces importan tanto como el texto. --dump json incluye el estado HTTP y el registro de la solicitud. Úselo cuando una obtención devuelva contenido vacío y necesite saber el motivo.

La estrategia de espera determina si obtiene contenido o una estructura vacía. --wait-until networkidle devuelve el resultado cuando la red deja de tener actividad. --wait-until domstable devuelve el resultado cuando el DOM deja de cambiar. Es una opción mejor para una página que realiza consultas en segundo plano y, por tanto, nunca deja de tener actividad. --wait-selector espera a que aparezca un selector que usted indique. Es la única estrategia que conoce algo sobre la página que está obteniendo, por lo que es la más fiable cuando conoce el objetivo.

Las capturas de pantalla y los PDF necesitan un diseño real, que está desactivado de forma predeterminada:

moli fetch --layout --dump screenshot https://example.com > page.png
moli fetch --layout --dump screenshot_full https://example.com > full-page.png
moli fetch --layout --dump pdf https://example.com > page.pdf

El README denomina LayoutPolicy::Mock a la política de diseño predeterminada: la geometría se simula y no se pinta nada, porque el diseño y el pintado son la mitad costosa de un navegador. Por eso las cifras de memoria anteriores tienen ese aspecto. También significa que un PNG vacío suele indicar que falta la opción --layout, no que la página esté dañada.

Para las URL que ha encontrado el agente, en lugar de las URL que usted eligió, añada --block-private-networks. Un agente que sigue enlaces leídos en una página puede ser inducido a obtener http://169.254.169.254/ para acceder a credenciales de instancias en la nube o a un puerto de base de datos en localhost que nunca debía exponerse a Internet. Esa opción rechaza la navegación hacia espacios de direcciones privadas, y --block-cidrs la restringe aún más. Cuando el trabajo consiste en rastrear páginas en lugar de leer una sola, la estructura de esa canalización se explica en alternativas autoalojadas a Firecrawl, y el paso anterior, encontrar las URL, se explica en una capacidad de búsqueda para agentes basada en SearXNG.

Apuntar un agente a Moli mediante CDP

Para un agente que navega y hace clic durante muchos pasos, ejecute el servidor por separado.

moli serve --host 127.0.0.1 --port 9222

127.0.0.1 y el puerto 9222 son los valores predeterminados, por lo que un moli serve sin argumentos ya se enlaza sólo a loopback. Aun así, especifique ambos valores en cualquier configuración permanente. La siguiente persona que lea el archivo de servicio no tendrá que recordar cuál era el valor predeterminado.

Compruebe el servidor antes de conectar un cliente:

curl -s http://127.0.0.1:9222/json/version

Un servidor operativo responde con un objeto JSON que contiene un campo webSocketDebuggerUrl. Esa URL es el punto al que se conecta un cliente CDP. curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused significa que no hay ningún proceso escuchando. En ese caso, lea el terminal desde el que inició el servidor o ejecute journalctl -u moli -n 50 si ya lo configuró como servicio. /json/list muestra los destinos abiertos y /json/protocol muestra los dominios que implementa esta compilación. Así puede comprobar si existe aquí un método CDP del que dependa.

Playwright se conecta a ese endpoint en lugar de iniciar su propio navegador:

import { chromium } from "playwright";

const browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();

await page.goto("https://example.com");
console.log(await page.locator("body").innerText());

await browser.close();

La línea importante es connectOverCDP, no chromium.launch(). Aquí no existe ningún proceso secundario de Chromium, por lo que executablePath y las opciones habituales de contenedor, como --no-sandbox, no tienen ningún efecto. Por la misma razón, las opciones de proxy, cookies y agente de usuario se pasan al servidor Moli mediante sus propias opciones. Espere una cobertura parcial de CDP, no todo el protocolo de Chrome. Un error explícito de método no compatible indica un límite del motor, no un error del código.

Dos opciones del servidor determinan lo que puede hacer el agente. --layout activa la geometría real, necesaria para hacer clic en coordenadas y tomar capturas de pantalla. --resource carga las imágenes, fuentes y archivos multimedia opcionales. Esto consume ancho de banda y memoria en cada carga de página, así que déjelo desactivado hasta que una página demuestre que lo necesita. --profile-dir conserva las cookies y el almacenamiento entre ejecuciones. Sin esta opción, cada ejecución es desechable.

ChartOne agent episode, Moli against Chromium, figures published by the Moli project
The data behind this chart
[
  {
    "engine": "Moli",
    "cdp_ready_ms": 34.85,
    "peak_pss_mib": 102.46,
    "processes": 1
  },
  {
    "engine": "Chromium",
    "cdp_ready_ms": 169.37,
    "peak_pss_mib": 348.82,
    "processes": 11
  }
]

En la carga de trabajo de agente de ejemplo del proyecto, Moli aceptó una conexión CDP después de 34.85 ms, frente a 169.37 ms para Chromium, con un PSS máximo (tamaño proporcional del conjunto residente; la memoria de las páginas compartidas se divide entre los procesos que las comparten) de 102.46 MiB, frente a 348.82 MiB. La diferencia estructural está en la última columna: 1 proceso, frente a 11. Un proceso es una única unidad que systemd debe supervisar y un único cgroup al que aplicar límites. Esto permite que la siguiente sección sea breve.

Ejecutar moli serve como servicio de systemd en loopback

Ejecute el servidor como servicio cuando un agente necesite un navegador a la espera. Siga usando moli fetch por URL cuando no lo necesite, porque un servidor inactivo sigue ocupando memoria.

No exponga el puerto 9222 en una interfaz pública. CDP no tiene ningún mecanismo de autenticación. Cualquiera que pueda acceder a ese puerto puede controlar el navegador y leer todo lo que el navegador pueda alcanzar, incluidas las cookies del directorio de perfil. Manténgalo en 127.0.0.1. Acceda desde otra máquina mediante un túnel SSH (ssh -L 9222:127.0.0.1:9222 user@your-vps) o a través de una interfaz VPN privada, y haga que el agente se conecte a http://127.0.0.1:9222 en su propio lado de ese túnel.

Cree un usuario de servicio y, después, el archivo de unidad:

sudo useradd --system --home-dir /var/lib/moli --shell /usr/sbin/nologin moli

Escriba /etc/systemd/system/moli.service:

[Unit]
Description=Moli headless browser CDP server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=moli
Group=moli
ExecStart=/usr/local/bin/moli serve --host 127.0.0.1 --port 9222 --profile-dir /var/lib/moli/profile --block-private-networks
Restart=on-failure
RestartSec=2
StateDirectory=moli
MemoryAccounting=yes
MemoryMax=768M
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now moli.service
systemctl status moli.service
curl -s http://127.0.0.1:9222/json/version

systemctl status debería mostrar active (running), y curl debería devolver el JSON de descubrimiento. ProtectSystem=strict monta todo el sistema de archivos como de solo lectura para esta unidad. Por eso StateDirectory=moli no es opcional aquí: crea /var/lib/moli, propiedad del usuario de servicio, y hace que esa ruta sea escribible. Una unidad que se inicia y después termina con un error de permisos en journalctl -u moli casi siempre intenta escribir en una ubicación que ProtectSystem acaba de hacer de solo lectura. Mueva esa ruta al directorio de estado.

MemoryMax=768M es lo que permite ejecutar esto de forma segura junto a su aplicación. La unidad obtiene su propio cgroup. Cuando ese cgroup supera su límite, el kernel termina algún proceso dentro de él y deja intacto el resto del sistema. El journal registra el evento:

moli.service: A process of this unit has been killed by the OOM killer.

Interprete esa línea como una señal para ajustar el tamaño. Las páginas pueden ser más pesadas de lo previsto o el límite puede ser demasiado bajo. Establezca el número a partir de una medición de sus propias páginas. Esta medición es el tema de la sección siguiente. Los mismos indicadores de contabilidad aíslan cualquier otro servicio del sistema, y limitar la memoria y la CPU con systemd funciona de la misma forma con el resto.

Mida usted mismo el consumo máximo de memoria

Las cifras publicadas proceden del hardware y de las páginas de otra persona. El consumo máximo de memoria determina si su servidor seguirá funcionando, y depende por completo de lo que cargue. Mida el consumo antes de dimensionar el servidor.

Para obtener datos puntuales, use el binario time, que informa de muchos más datos que el builtin del shell con el mismo nombre:

sudo apt update && sudo apt install -y time
/usr/bin/time -v moli fetch --dump markdown --wait-until networkidle https://example.com > /dev/null

La salida termina con un bloque de estadísticas de recursos que incluye Maximum resident set size (kbytes). Divídalo entre 1024 para obtener MiB. Ejecútelo sobre diez páginas que su agente visite realmente, no sobre example.com, y conserve el resultado más alto en lugar del promedio, porque el OOM killer reacciona a los picos.

Para el servicio, lea el contador que el kernel ya mantiene para su cgroup:

cat /sys/fs/cgroup/system.slice/moli.service/memory.peak
systemd-cgtop -m

memory.peak es un recuento de bytes y representa el valor máximo registrado desde el último inicio de la unidad, por lo que un reinicio lo restablece. MemoryMax debe estar por encima de esa cifra, con margen adicional para la página más pesada que todavía no haya visitado. systemd-cgtop -m muestra el uso actual por unidad. Es la forma más rápida de identificar qué servicio del servidor consume más recursos en ese momento.

¿Dónde falla Moli y cuándo sigue siendo necesario Chrome?

El proyecto también ejecuta una evaluación comparativa de 1,308 tareas comparables de automatización del navegador y publica la puntuación de varios motores.

ChartLexbench headless browser suite, 1,308 tasks, figures published by the Moli project
The data behind this chart
[
  {
    "engine": "Chrome",
    "success_rate_pct": 99.85
  },
  {
    "engine": "Moli 0.1.1",
    "success_rate_pct": 81.88
  },
  {
    "engine": "Kitesurf",
    "success_rate_pct": 62.08
  },
  {
    "engine": "Lightpanda",
    "success_rate_pct": 53.29
  },
  {
    "engine": "Obscura",
    "success_rate_pct": 44.88
  }
]

Entre esos 5 motores, Moli 0.1.1 completó el 81.88 por ciento de las tareas y Chrome, el motor de referencia, completó el 99.85 por ciento. El propio proyecto calcula esta puntuación con su conjunto de pruebas, así que debe interpretarse como una afirmación y no como un resultado independiente.

La interpretación práctica es sencilla. Aproximadamente una de cada cinco tareas que Chrome completó falló en Moli. Si el agente visita un conjunto fijo de páginas que usted controla, esa proporción aporta poca información, porque las páginas funcionan o no funcionan y puede comprobarlo hoy mismo. Si el agente navega por la web abierta, es una tasa de fallos real que debe contemplar en el diseño.

Los fallos se pueden prever a partir del alcance declarado del proyecto.

  • Aplicaciones que dibujan su interfaz en un elemento Canvas en lugar de usar el DOM, porque la fidelidad de Canvas está explícitamente fuera de alcance
  • Cualquier función que necesite WebGL o composición mediante GPU, porque no existe un compositor de GPU
  • Vídeo protegido con DRM y reproducción multimedia exigente
  • Pruebas visuales que comprueban capturas de pantalla con coincidencia exacta de píxeles con Chrome, ya que la paridad con Chrome no es un objetivo

La otra cifra que cita el proyecto, una ejecución completa con 1.612 millones de pruebas de la plataforma web superadas, describe la cobertura de estándares. No garantiza el funcionamiento en los sitios que visitará el agente. Una página puede utilizar únicamente estándares bien compatibles y aun así fallar en una comprobación para bots; ninguna puntuación del motor cubre ese caso.

Por tanto, mantenga el fallback en el diseño. Envíe primero todas las URL a Moli. Cuando una página devuelva contenido vacío o un selector no aparezca nunca, vuelva a intentar esa URL con Playwright controlando un Chrome real, en una máquina más grande o en un horario en el que un proceso de 773 MiB resulte asumible. La mayoría de los agentes pasa la mayor parte del tiempo en páginas normales, por lo que el motor pequeño gestiona el volumen y el motor costoso cubre los casos excepcionales.

FAQ

¿Puede Moli sustituir a headless Chrome para mi agente?

Para leer páginas, extraer texto y hacer clic en elementos normales, por lo general sí. En el benchmark propio del proyecto, completó 81.88 por ciento de 1,308 tareas, frente al 99.85 por ciento de Chrome. Por tanto, aproximadamente una de cada cinco tareas requiere algo que Moli no hace. Las brechas conocidas son las aplicaciones renderizadas con Canvas, WebGL y el vídeo con DRM. Dirija esas URL a Chrome real en lugar de cambiar todo de nuevo.

¿Cuánta RAM necesita Moli en un VPS?

El proyecto informa de un RSS mediano de 73 MiB en un rastreo de 192 URL y de un PSS máximo de 102.46 MiB en un episodio de agente de muestra, frente a una mediana de 773 MiB para headless Chrome. Esas son sus cifras en sus páginas. Mida el consumo propio con /usr/bin/time -v alrededor de una llamada a moli fetch para un uso puntual, o lea /sys/fs/cgroup/system.slice/moli.service/memory.peak para el servicio. Después, establezca MemoryMax por encima de la cifra máxima observada.

¿Es seguro exponer el puerto 9222 a Internet?

No. CDP no tiene autenticación. Cualquiera que pueda acceder a ese puerto puede controlar el navegador y leer todo lo que el navegador pueda alcanzar. Mantenga --host 127.0.0.1 y acceda al endpoint desde otra máquina mediante un túnel SSH o a través de una interfaz VPN privada. Si debe enlazar otra dirección, asígnela a una interfaz privada y controle el acceso con el firewall.

¿Por qué mi captura de pantalla está vacía o el clic no cae sobre ningún elemento?

El diseño está desactivado de forma predeterminada. El README indica que la política predeterminada es LayoutPolicy::Mock. Por tanto, la geometría de los elementos no es real y todo lo que depende de una caja en la página no puede funcionar. Inicie el servidor con moli serve --layout o añada --layout a moli fetch. Entonces funcionarán la captura de pantalla y las rutas basadas en coordenadas. Las imágenes ausentes requieren otro flag: --resource.

¿Qué versión de Moli debo instalar?

Fije una versión y registre cuál es. En agosto de 2026, la release actual es 1.0.1, mientras que las cifras del benchmark publicadas por el proyecto se midieron con 0.1.1. Por tanto, no son intercambiables al comparar resultados con otra persona. Descargue el moli-x86_64-unknown-linux-gnu.tar.gz de ese tag e instale el binario manualmente en lugar de depender del instalador de shell. El instalador resuelve la release más reciente, no el tag desde el que la descargó. Después, confirme la versión con moli --version.