Auto-héberger Moli, navigateur headless pour agents
Chrome headless est le plus gros processus de votre petit VPS. Installez Moli, exposez CDP en loopback et repérez les pages qui échouent.
Un navigateur headless adapté à un petit VPS
Moli est un navigateur headless pour les agents d’IA. Il est suffisamment léger pour être auto-hébergé sur un VPS où headless Chrome ne tiendrait pas. Il s’agit d’un moteur de navigateur écrit en Rust, et non d’un wrapper autour de Chromium. Il implémente le Chrome DevTools Protocol (CDP), le protocole utilisé par votre bibliothèque d’automatisation. Installez un seul binaire, exécutez moli serve, puis configurez Playwright ou le code de votre agent pour utiliser http://127.0.0.1:9222.
Prenez connaissance des compromis avant toute installation. Le projet définit clairement son périmètre : pas de navigateur graphique, pas de compositeur GPU, pas de parité pixel par pixel avec Chrome, et pas de prise en charge haute fidélité de Canvas ou de la lecture multimédia. Les pages qui en dépendent échoueront. Chrome exécuté avec Playwright reste la solution de repli. La dernière section explique comment déterminer quelles pages en ont besoin.
Toutes les commandes ci-dessous proviennent du README du projet et de ses fichiers de skills publiés, vérifiés en août 2026. Tous les nombres des graphiques correspondent à des chiffres publiés par le projet concernant son propre moteur, et non à des mesures effectuées sur ce site. Chaque légende de graphique le précise. Si vous choisissez encore un moteur, l’étude plus large des navigateurs headless pour les agents sur un VPS présente les solutions alternatives.
Pourquoi Chrome headless utilise-t-il autant de mémoire ?
Chrome est un navigateur multiprocessus. Chaque onglet et chaque iframe intersite dispose de son propre processus de rendu, et chaque renderer possède son propre tas V8 ainsi que ses propres buffers graphiques. Cette conception convient à un poste de travail : le crash d’un onglet ne doit pas entraîner la fermeture de toute la fenêtre. Sur un VPS de 2 GB, une seule étape de navigation peut donc consommer plus de mémoire que l’application que vous exécutez réellement.
Le projet a parcouru 192 URL publiques variées avec quatre moteurs et a publié le résultat.
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 a renvoyé 101 pages utiles, contre 103 pour Moli. Sur cet échantillon, les deux moteurs ont donc consulté une part à peu près équivalente du Web. La différence concerne la mémoire : le RSS médian (resident set size, c’est-à-dire la mémoire qu’un processus occupe réellement en RAM) est de 773 MiB pour Chrome, contre 73 MiB pour Moli. Cette tendance est crédible, car elle découle de l’architecture multiprocessus. Vous ne devez toutefois pas supposer que le ratio exact sera le même sur vos pages.
Ce n’est pas la médiane qui pose problème, mais le pic. Lorsqu’une machine de 2 GB n’a plus de mémoire disponible, le kernel sélectionne un processus et le tue. L’événement est alors enregistré dans dmesg -T ou 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:0Votre agent ne voit jamais cette ligne. Il voit un navigateur qui ne répond plus, généralement sous la forme d’une erreur Playwright telle que page.goto: Page crashed, ou d’une target fermée. Cette erreur ne mentionne pas la mémoire. C’est pourquoi l’OOM (out of memory) killer est la première chose à vérifier lorsqu’un agent échoue de manière aléatoire sur une petite machine. Dimensionner la machine pour le pic revient à choisir la RAM et le CPU d’un VPS pour agent.
Installer le binaire Moli en épinglant une version
Le projet publie un installateur shell et des archives tar précompilées dans les releases GitHub. En août 2026, la release actuelle est la 1.0.1, publiée le 18 août 2026. Les chiffres de benchmark cités dans ce guide ont été mesurés par le projet avec la 0.1.1. Considérez-les donc comme une indication générale du comportement du moteur, et non comme une garantie pour le build que vous installez.
Épinglez la version. Un installateur qui résout toujours latest fait passer votre agent à un autre moteur de navigateur lors du prochain rebuild. Une modification du comportement du navigateur doit être planifiée, et non découverte par hasard.
L’installateur shell est la méthode la plus rapide pour commencer. Il faut toutefois le lire avant de l’exécuter.
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.shLisez le script avant de l’exécuter. Il est court. Il sélectionne une archive depuis votre uname -m, puis décompresse un seul binaire dans ~/.local/bin. Sur x86_64, il utilise moli-x86_64-unknown-linux-gnu.tar.gz. Sur un serveur Arm, il utilise l’archive aarch64. Les offres VPS Arm et x86 sont donc couvertes. Définissez MOLI_INSTALL_DIR pour installer le binaire ailleurs. Vérifiez la version qu’il résout : il s’agit de la release la plus récente, et non du tag depuis lequel vous avez récupéré le script. Cela convient pour un premier essai, mais pas pour un rebuild reproductible.
Pour toute installation permanente, faites manuellement ce que fait l’installateur et indiquez vous-même l’archive exacte. Vous pourrez ainsi placer le binaire dans un répertoire accessible par un service système. Vous éviterez aussi de transférer un script téléchargé vers un shell par pipe.
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 --versionmoli --version qui affiche la version épinglée constitue l’intégralité de la vérification. moli: command not found immédiatement après l’installation signifie que le répertoire d’installation ne figure pas dans votre PATH. L’installateur affiche alors une ligne indiquant le répertoire à ajouter.
Extraction ponctuelle avec moli fetch
Beaucoup de tâches confiées à un agent consistent à « charger cette URL et me dire ce qu’elle contient ». Aucun serveur n’est nécessaire dans ce cas. moli fetch démarre le moteur, charge une page, écrit un artefact sur la sortie standard, puis se termine. Rien ne conserve donc de mémoire entre les appels.
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.jsonLa première commande affiche la page au format Markdown, en commençant par # Example Domain. Le Markdown est le format le plus économique à transmettre à un modèle, car il supprime le balisage et conserve le texte. semantic_tree_text conserve les rôles et la structure. C’est ce qu’il faut privilégier sur une page riche en navigation, où les liens sont aussi importants que le texte. --dump json fournit le code d’état HTTP et la trace de la requête. Utilisez-le lorsqu’une récupération renvoie une page vide et que vous devez en déterminer la cause.
La stratégie d’attente détermine si vous obtenez le contenu ou une structure vide. --wait-until networkidle renvoie le résultat lorsque le réseau cesse son activité. --wait-until domstable renvoie le résultat lorsque le DOM cesse de changer. C’est le meilleur choix pour une page qui interroge régulièrement un backend et ne devient donc jamais complètement inactive. --wait-selector attend un sélecteur que vous indiquez. C’est la seule stratégie qui tient compte du contenu de la page récupérée. Elle est donc la plus fiable lorsque vous connaissez la cible.
Les captures d’écran et les PDF nécessitent une mise en page réelle, désactivée par défaut :
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.pdfLe README nomme la stratégie de mise en page par défaut LayoutPolicy::Mock : la géométrie est simulée et rien n’est rendu, car la mise en page et le rendu constituent la partie la plus coûteuse d’un navigateur. Ce comportement par défaut explique les valeurs de mémoire indiquées plus haut. Il signifie également qu’un fichier PNG vide est généralement dû à l’absence de l’option --layout, et non à une page défectueuse.
Pour les URL trouvées par votre agent plutôt que choisies par vous, ajoutez --block-private-networks. Un agent qui suit les liens lus sur une page peut être amené à récupérer http://169.254.169.254/ pour obtenir des identifiants d’instances cloud, ou à accéder à un port de base de données sur localhost qui n’était pas destiné à être exposé sur le Web. Cette option interdit la navigation vers l’espace d’adressage privé, et --block-cidrs applique une restriction supplémentaire. Lorsque la tâche consiste à explorer un site plutôt qu’à lire une seule page, la structure de ce pipeline est présentée dans des alternatives auto-hébergées à Firecrawl. L’étape précédente, qui consiste à trouver les URL, est présentée dans une compétence de recherche pour agents basée sur SearXNG.
Diriger un agent vers Moli via CDP
Pour un agent qui navigue et effectue de nombreux clics, exécutez plutôt le serveur.
moli serve --host 127.0.0.1 --port 9222127.0.0.1 et le port 9222 sont les valeurs par défaut. Un simple moli serve est donc déjà lié à la loopback uniquement. Indiquez-les quand même tous les deux dans toute configuration persistante. Ainsi, la personne qui consultera ensuite votre fichier de service n’aura pas à se souvenir de la valeur par défaut.
Vérifiez le serveur avant d’y connecter un client :
curl -s http://127.0.0.1:9222/json/versionUn serveur opérationnel répond avec un objet JSON contenant un champ webSocketDebuggerUrl. Cette URL est celle à laquelle un client CDP se connecte. curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused signifie que rien n’écoute. Consultez donc le terminal dans lequel vous avez démarré le serveur, ou utilisez journalctl -u moli -n 50 s’il s’agit d’un service. /json/list liste les cibles ouvertes et /json/protocol liste les domaines implémentés par ce build. Cela permet de vérifier qu’une méthode CDP dont vous dépendez est disponible.
Playwright se connecte à cet endpoint au lieu de lancer son propre navigateur :
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 ligne importante est connectOverCDP, pas chromium.launch(). Aucun processus Chromium enfant n’est lancé ici. executablePath et les options habituelles des conteneurs, comme --no-sandbox, n’ont donc rien à contrôler. Pour la même raison, transmettez les paramètres de proxy, de cookie et de user-agent au serveur Moli sous la forme de ses propres options. La couverture CDP est partielle, et non équivalente à l’ensemble du protocole Chrome. Une erreur explicite indiquant qu’une méthode n’est pas prise en charge marque une limite du moteur, pas un bug dans votre code.
Deux options du serveur déterminent ce que l’agent peut faire. --layout active la géométrie réelle, nécessaire aux clics par coordonnées et aux captures d’écran. --resource récupère les images, les polices et les médias facultatifs. Cela consomme de la bande passante et de la mémoire à chaque chargement de page. Laissez donc cette option désactivée jusqu’à ce qu’une page démontre qu’elle est nécessaire. --profile-dir conserve les cookies et le stockage entre les exécutions. Sans cette option, chaque exécution est éphémère.
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
}
]Sur la charge de travail d’agent fournie en exemple par le projet, Moli a accepté une connexion CDP après 34.85 ms, contre 169.37 ms pour Chromium, avec un PSS maximal (proportional set size, mémoire comptabilisée avec les pages partagées réparties entre les processus qui les partagent) de 102.46 MiB, contre 348.82 MiB. La différence structurelle apparaît dans la dernière colonne : 1 processus contre 11. Un seul processus constitue une unité à superviser pour systemd et un seul cgroup à limiter. C’est ce qui permet de garder la section suivante courte.
Exécuter moli serve comme service systemd sur loopback
Exécutez le serveur comme service lorsqu’un agent a besoin qu’un navigateur reste en attente. Continuez à utiliser moli fetch par URL lorsque ce n’est pas le cas, car un serveur inactif conserve sa mémoire.
N’exposez pas le port 9222 sur une interface publique. CDP ne comporte aucune étape d’authentification. Toute personne pouvant atteindre ce port peut piloter le navigateur et lire tout ce à quoi le navigateur peut accéder, y compris les cookies présents dans votre répertoire de profil. Maintenez-le sur 127.0.0.1. Depuis une autre machine, utilisez un tunnel SSH (ssh -L 9222:127.0.0.1:9222 user@your-vps) ou une interface VPN privée, puis laissez l’agent se connecter à http://127.0.0.1:9222 de son côté du tunnel.
Créez un utilisateur de service, puis le fichier d’unité :
sudo useradd --system --home-dir /var/lib/moli --shell /usr/sbin/nologin moliÉcrivez /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.targetsudo systemctl daemon-reload
sudo systemctl enable --now moli.service
systemctl status moli.service
curl -s http://127.0.0.1:9222/json/versionsystemctl status doit afficher active (running), et curl doit renvoyer le JSON de découverte. ProtectSystem=strict monte l’ensemble du système de fichiers en lecture seule pour cette unité. C’est pourquoi StateDirectory=moli est indispensable ici : il crée /var/lib/moli, qui appartient à l’utilisateur de service, et rend ce seul chemin accessible en écriture. Une unité qui démarre puis s’arrête avec une erreur de permission dans journalctl -u moli essaie presque toujours d’écrire à un emplacement que ProtectSystem vient de rendre accessible en lecture seule. Déplacez donc ce chemin sous le répertoire d’état.
MemoryMax=768M permet d’exécuter ce service en toute sécurité à côté de votre application. L’unité dispose de son propre cgroup. Lorsque ce cgroup dépasse sa limite, le kernel tue un processus qui en fait partie et laisse le reste du serveur intact. Le journal l’indique :
moli.service: A process of this unit has been killed by the OOM killer.Interprétez cette ligne comme un indicateur de dimensionnement. Les pages sont soit plus lourdes que prévu, soit la limite est trop basse. Définissez cette valeur à partir d’une mesure effectuée sur vos propres pages ; c’est l’objet de la section suivante. Les mêmes options de comptabilisation limitent également tout autre service du serveur, et limiter la mémoire et le CPU avec systemd fonctionne de la même manière pour les autres services.
Mesurez vous-même le pic de mémoire
Les chiffres publiés proviennent du matériel et des pages de quelqu’un d’autre. Le pic de mémoire détermine si votre serveur tient la charge, et il dépend entièrement de ce que vous chargez. Mesurez avant de dimensionner.
Pour une récupération ponctuelle, utilisez le binaire time. Il fournit beaucoup plus d’informations que le builtin shell du même nom :
sudo apt update && sudo apt install -y time
/usr/bin/time -v moli fetch --dump markdown --wait-until networkidle https://example.com > /dev/nullLa sortie se termine par un bloc de statistiques de ressources qui inclut Maximum resident set size (kbytes). Divisez cette valeur par 1024 pour obtenir des MiB. Exécutez la commande sur dix pages que votre agent consulte réellement, plutôt que sur example.com, et conservez le résultat le plus élevé au lieu de la moyenne, car l’OOM killer réagit aux pics.
Pour le service, lisez le compteur que le kernel conserve déjà pour son cgroup :
cat /sys/fs/cgroup/system.slice/moli.service/memory.peak
systemd-cgtop -mmemory.peak est une valeur en octets. Il s’agit du pic d’utilisation depuis le dernier démarrage de l’unité, donc un redémarrage le réinitialise. Cette valeur doit être inférieure à MemoryMax, avec une marge supplémentaire pour la page la plus lourde que vous n’avez pas encore visitée. systemd-cgtop -m affiche l’utilisation actuelle par unité. C’est le moyen le plus rapide de voir quel service consomme le plus de ressources sur le serveur aujourd’hui.
Où Moli échoue-t-il, et quand avez-vous encore besoin de Chrome ?
Le projet exécute également une évaluation comparative de 1,308 tâches comparables d’automatisation de navigateur et publie le score de plusieurs moteurs.
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
}
]Parmi ces 5 moteurs, Moli 0.1.1 a terminé 81.88 pour cent des tâches, tandis que Chrome, le moteur de référence, en a terminé 99.85 pour cent. Le projet évalue ici son propre moteur sur sa propre suite de tests. Il faut donc considérer ce résultat comme une affirmation, et non comme un résultat indépendant.
L’interprétation pratique est simple. Environ une tâche sur cinq a échoué avec Moli alors que Chrome l’a terminée. Si votre agent consulte un ensemble fixe de pages que vous contrôlez, ce ratio vous apprend très peu : vos pages fonctionnent ou non, et vous pouvez le vérifier dans l’après-midi. Si votre agent parcourt le Web ouvert, il s’agit d’un véritable taux d’échec dont vous devez tenir compte dans la conception.
Les échecs sont prévisibles au vu du périmètre annoncé par le projet.
- Les applications qui dessinent leur interface dans un élément Canvas plutôt que dans le DOM, car la fidélité de Canvas est explicitement hors périmètre
- Tout ce qui nécessite WebGL ou une composition GPU, car aucun compositeur GPU n’est disponible
- Les vidéos protégées par DRM et la lecture de contenus multimédias exigeants
- Les tests visuels qui comparent des captures d’écran au pixel près avec Chrome, car la parité avec Chrome n’est pas un objectif
L’autre chiffre cité par le projet, à savoir la réussite d’une exécution complète de 1.612 million de tests de plateforme Web, concerne la couverture des standards. Il ne garantit pas le fonctionnement des sites que votre agent consultera. Une page peut n’utiliser que des standards bien pris en charge et échouer malgré tout à un contrôle anti-bot. Aucun score de moteur ne couvre ce cas.
Prévoyez donc un moteur de secours dès la conception. Envoyez d’abord chaque URL à Moli. Lorsqu’une page revient vide ou qu’un sélecteur n’apparaît jamais, réessayez cette URL avec Playwright pilotant un véritable Chrome, sur une machine plus puissante ou selon une planification où un processus de 773 MiB reste acceptable. La plupart des agents passent l’essentiel de leur temps sur des pages ordinaires. Le petit moteur traite donc le volume, et le moteur coûteux prend en charge les cas difficiles.
FAQ
Moli peut-il remplacer Chrome headless pour mon agent ?
Pour lire des pages, extraire du texte et effectuer des clics ordinaires, généralement oui. Sur le benchmark du projet, qui porte sur 1,308 tâches, il a réussi 81.88 pour cent des tâches, contre 99.85 pour cent pour Chrome ; environ une tâche sur cinq nécessite donc une fonction que Moli ne fournit pas. Les applications rendues avec Canvas, WebGL et les vidéos protégées par DRM sont les principales limites connues. Dirigez ces URL vers un véritable Chrome au lieu de tout basculer à nouveau vers Chrome.
De combien de RAM Moli a-t-il besoin sur un VPS ?
Le projet indique un RSS médian de 73 MiB sur une exploration de 192 URL, ainsi qu’un PSS maximal de 102.46 MiB lors d’un épisode d’agent d’exemple, contre une valeur médiane de 773 MiB pour Chrome headless. Ce sont les valeurs relevées sur les pages du projet. Mesurez votre propre consommation avec /usr/bin/time -v autour d’un appel à moli fetch pour une exécution ponctuelle, ou consultez /sys/fs/cgroup/system.slice/moli.service/memory.peak pour le service, puis définissez MemoryMax au-dessus de la valeur maximale observée.
Est-il sûr d’exposer le port 9222 sur Internet ?
Non. CDP ne fournit aucune authentification. Toute personne pouvant atteindre ce port peut contrôler votre navigateur et lire tout ce auquel le navigateur peut accéder. Conservez --host 127.0.0.1 et accédez au point de terminaison depuis une autre machine au moyen d’un tunnel SSH ou d’une interface VPN privée. Si vous devez lier une autre adresse, utilisez une interface privée et contrôlez l’accès avec le pare-feu.
Pourquoi ma capture d’écran est-elle vide, ou pourquoi mon clic n’aboutit-il à rien ?
La mise en page est désactivée par défaut. Le README indique que la stratégie par défaut est LayoutPolicy::Mock. La géométrie des éléments n’est donc pas réelle, et tout ce qui dépend d’un élément sur la page ne peut pas fonctionner. Démarrez le serveur avec moli serve --layout, ou ajoutez --layout à moli fetch. La capture d’écran et les fonctions utilisant les coordonnées fonctionneront alors. Les images manquantes relèvent d’une autre option : --resource.
Quelle version de Moli dois-je installer ?
Épinglez une version et consignez-la. En août 2026, la version actuelle est 1.0.1, tandis que les chiffres du benchmark publiés par le projet ont été mesurés avec la version 0.1.1. Les deux versions ne sont donc pas interchangeables lorsque vous comparez vos résultats avec ceux d’une autre personne. Téléchargez le moli-x86_64-unknown-linux-gnu.tar.gz de cette balise et installez vous-même le binaire au lieu d’utiliser l’installateur shell. Celui-ci sélectionne la dernière version disponible, et non la balise depuis laquelle vous avez effectué le téléchargement. Vérifiez ensuite avec moli --version.