SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-21

Come eseguire Moli self-hosted per agenti AI

Scopri come installare Moli su un piccolo VPS, esporre CDP su loopback e collegarlo a Playwright, con limiti e casi in cui Chrome resta necessario.

Un browser headless adatto a un VPS di piccole dimensioni

Moli è un browser headless per agenti AI, abbastanza leggero da poter essere eseguito su un VPS in cui Chrome headless non entrerebbe. È un motore browser scritto in Rust, non un wrapper per Chromium, e implementa il Chrome DevTools Protocol (CDP), il protocollo già utilizzato dalla libreria di automazione. Basta installare un singolo binario, eseguire moli serve, quindi indicare http://127.0.0.1:9222 a Playwright o al codice del proprio agente.

Prima di installare qualsiasi componente, valutate i compromessi. Il progetto dichiara chiaramente il proprio ambito: nessun browser con GUI, nessun compositore GPU, nessuna parità pixel per pixel con Chrome e nessuna elevata fedeltà per Canvas o la riproduzione multimediale. Le pagine che richiedono queste funzionalità non funzioneranno. Chrome reale con Playwright resta l'alternativa di fallback; l'ultima sezione spiega come determinare quali pagine ne hanno bisogno.

Ogni comando riportato di seguito proviene dal README del progetto e dai relativi skill file pubblicati, verificati ad agosto 2026. Ogni numero nei grafici è un dato pubblicato dal progetto sul proprio motore, non una misurazione eseguita su questo sito; ogni didascalia specifica questa informazione. Se state ancora scegliendo un motore, l'analisi più ampia dei browser headless per agenti su un VPS descrive le alternative.

Perché Chrome headless usa così tanta memoria?

Chrome è un browser multiprocesso. Ogni scheda e ogni iframe cross-site utilizza un proprio processo renderer, e ogni renderer ha il proprio heap V8 e i propri buffer grafici. Questo design è corretto su un desktop, dove il crash di una scheda non deve causare la chiusura dell'intera finestra. Su un VPS con 2 GB, però, una singola fase di navigazione può consumare più memoria dell'applicazione effettivamente in esecuzione.

Il progetto ha analizzato 192 URL pubblici eterogenei con quattro engine e ha pubblicato il risultato.

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 ha restituito 101 pagine utili, mentre Moli ne ha restituite 103; su questo campione, quindi, i due engine hanno analizzato una quota del Web approssimativamente equivalente. La differenza emerge nella memoria: un RSS mediano (resident set size, la memoria che un processo mantiene effettivamente nella RAM) di 773 MiB per Chrome, contro 73 MiB per Moli. Questa differenza è plausibile perché deriva dall'architettura basata sui processi. Non bisogna però assumere che il rapporto esatto sia lo stesso sulle proprie pagine.

La mediana non è il valore che causa i problemi. Il valore critico è il picco. Quando un sistema con 2 GB esaurisce la memoria, il kernel seleziona un processo e lo termina; l'evento viene registrato in 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

Il tuo agent non vede quella riga. Vede un browser che ha smesso di rispondere, in genere con un errore di Playwright come page.goto: Page crashed oppure con un target chiuso. Nessuno di questi errori indica la memoria, perciò l'OOM (out of memory) killer è il primo elemento da controllare quando un agent ha errori casuali su un sistema con poche risorse. Dimensionare il sistema per il picco equivale a scegliere RAM e CPU per un VPS destinato a un agent.

Installare il binario Moli con una versione bloccata

Il progetto pubblica un installer shell e archivi tar precompilati nelle release di GitHub. Ad agosto 2026 la release corrente è 1.0.1, pubblicata il 18 agosto 2026. I valori di benchmark riportati in questa guida sono stati misurati dal progetto sulla versione 0.1.1. Considerali quindi un’indicazione generale del comportamento dell’engine, non una garanzia per la build installata.

Blocca la versione. Un installer che risolve sempre latest sposta l’agent su un engine del browser diverso al rebuild successivo. Una variazione nel comportamento del browser va pianificata, non scoperta durante l’esecuzione.

L’installer shell è il modo più rapido per iniziare. Prima di eseguirlo, però, conviene leggerlo.

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

Leggi lo script prima di eseguirlo. È breve. Seleziona un archivio da uname -m, quindi estrae un singolo binario in ~/.local/bin. Su x86_64 usa moli-x86_64-unknown-linux-gnu.tar.gz, mentre su un server Arm usa l’archivio aarch64. Sono quindi supportati sia i piani VPS Arm sia quelli x86. Imposta MOLI_INSTALL_DIR per installare il binario in un’altra posizione. Verifica quale versione risolve: la release più recente, non il tag da cui hai scaricato lo script. Questo comportamento va bene per una prima prova, ma non per un rebuild ripetibile.

Per qualsiasi installazione permanente, esegui manualmente le stesse operazioni dell’installer e indica tu l’archivio esatto. In questo modo puoi collocare il binario in una posizione raggiungibile da un servizio di sistema. Inoltre, non devi inoltrare uno script scaricato direttamente a una shell.

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 che stampa la versione bloccata è sufficiente per il controllo. moli: command not found subito dopo l’installer indica che la directory di installazione non è inclusa in PATH. L’installer stampa una riga con il nome della directory da aggiungere.

Estrazione singola con moli fetch

Molti dei compiti che un agente assegna a un browser consistono nel «carica questo URL e dimmi che cosa contiene». Per questo non serve alcun server. moli fetch avvia il motore, carica una pagina, scrive un artifact su standard output e termina; tra una chiamata e l’altra non resta nulla in memoria.

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

Il primo comando stampa la pagina in Markdown, iniziando con # Example Domain. Il Markdown è il formato più economico da fornire a un modello, perché rimuove il markup e conserva il testo. semantic_tree_text mantiene ruoli e struttura, ed è quindi adatto alle pagine ricche di navigazione, dove i link sono importanti quanto il testo. --dump json include lo stato HTTP e la traccia della richiesta; usalo quando un fetch restituisce una risposta vuota e devi capire il motivo.

La strategia di attesa determina se ottieni il contenuto oppure uno shell vuoto. --wait-until networkidle restituisce il risultato quando il traffico di rete si arresta. --wait-until domstable restituisce il risultato quando il DOM smette di cambiare; è la scelta migliore per una pagina che esegue polling in background e quindi non diventa mai completamente inattiva. --wait-selector attende un selettore specificato. È l’unica strategia che conosce qualcosa della pagina che stai recuperando; per questo è la più affidabile quando conosci l’elemento di destinazione.

Gli screenshot e i PDF richiedono un layout effettivo, che per impostazione predefinita è disabilitato:

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

Il README indica come criterio di layout predefinito LayoutPolicy::Mock: la geometria viene simulata e non viene eseguito alcun rendering, perché layout e rendering costituiscono la parte più costosa di un browser. Questa impostazione predefinita spiega i valori di memoria indicati sopra. Significa inoltre che un PNG vuoto di solito indica l’assenza del flag --layout, non un problema della pagina.

Per gli URL trovati dall’agente, invece di quelli scelti da te, aggiungi --block-private-networks. Un agente che segue i link letti su una pagina potrebbe essere indotto a recuperare http://169.254.169.254/ per ottenere le credenziali di un’istanza cloud oppure una porta database su localhost che non doveva essere esposta sul web. Quel flag impedisce la navigazione verso spazi di indirizzi privati, mentre --block-cidrs applica una restrizione ulteriore. Quando il compito consiste nel crawling anziché nella lettura di una singola pagina, la struttura della pipeline è descritta in alternative self-hosted a Firecrawl; il passaggio precedente, cioè la ricerca degli URL, è descritto in una skill di ricerca basata su SearXNG per agenti.

Fai puntare un agent a Moli tramite CDP

Per un agent che esegue molte operazioni di navigazione e clic, avvia invece il server.

moli serve --host 127.0.0.1 --port 9222

127.0.0.1 e la porta 9222 sono i valori predefiniti, quindi un semplice moli serve è già in ascolto soltanto sull'interfaccia loopback. Tuttavia, nelle configurazioni permanenti indica sempre entrambi i valori, così chi leggerà in seguito il file del servizio non dovrà ricordare quali fossero i valori predefiniti.

Verifica il server prima di collegarvi un client:

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

Un server funzionante risponde con un oggetto JSON che contiene un campo webSocketDebuggerUrl. Il relativo URL è quello a cui si collega un client CDP. curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused indica che non è in ascolto alcun processo, quindi controlla il terminale da cui hai avviato il server oppure esegui journalctl -u moli -n 50 se il server è un servizio. /json/list elenca le destinazioni aperte e /json/protocol elenca i domini implementati da questa build. In questo modo puoi verificare se esiste un metodo CDP da cui dipendi.

Playwright si collega a quell'endpoint invece di avviare un browser autonomamente:

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 riga importante è connectOverCDP, non chromium.launch(). In questo caso non esiste alcun processo Chromium figlio, quindi executablePath e i consueti flag dei container, come --no-sandbox, non hanno alcun effetto. Per lo stesso motivo, le impostazioni di proxy, cookie e user agent vanno passate al server Moli tramite i relativi flag. La copertura CDP è parziale, non equivalente all'intero protocollo Chrome: un errore esplicito relativo a un metodo non supportato indica un limite del motore, non un bug del codice.

Due flag del server determinano le operazioni che l'agent può eseguire. --layout abilita la geometria reale, necessaria per i clic basati sulle coordinate e per gli screenshot. --resource scarica immagini, font e contenuti multimediali opzionali. Questa opzione consuma banda e memoria a ogni caricamento di pagina, quindi lasciala disattivata finché una pagina non dimostra di averne bisogno. --profile-dir conserva cookie e storage tra un'esecuzione e l'altra. Senza questo flag, ogni esecuzione è temporanea.

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
  }
]

Nel carico di lavoro dell'agent di esempio del progetto, Moli ha accettato una connessione CDP dopo 34.85 ms, rispetto a 169.37 ms per Chromium, con un PSS di picco (proportional set size, cioè la memoria con le pagine condivise ripartita tra i processi che le condividono) di 102.46 MiB, rispetto a 348.82 MiB. La differenza strutturale è nell'ultima colonna: 1 processo rispetto a 11. Un solo processo corrisponde a un'unica unità che systemd deve supervisionare e a un unico cgroup a cui applicare un limite. Per questo la sezione successiva è breve.

Eseguire moli serve come servizio systemd su loopback

Esegui il server come servizio quando un agente deve avere un browser in attesa. Continua a usare moli fetch per URL quando non è necessario, perché un server inattivo occupa comunque memoria.

Non esporre la porta 9222 su un'interfaccia pubblica. CDP non prevede alcun meccanismo di autenticazione. Chiunque possa raggiungere quella porta può controllare il browser e leggere tutto ciò che il browser può raggiungere, inclusi i cookie presenti nella directory del profilo. Mantienilo su 127.0.0.1. Raggiungilo da un'altra macchina tramite un tunnel SSH (ssh -L 9222:127.0.0.1:9222 user@your-vps) o attraverso un'interfaccia VPN privata, quindi fai connettere l'agente a http://127.0.0.1:9222 dal proprio lato del tunnel.

Crea un utente di servizio, quindi il file dell'unità:

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

Scrivi /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 dovrebbe mostrare active (running) e curl dovrebbe restituire il JSON di discovery. ProtectSystem=strict monta l'intero filesystem in sola lettura per questa unità. Per questo StateDirectory=moli è obbligatorio: crea /var/lib/moli, lo assegna all'utente di servizio e rende scrivibile solo quel percorso. Un'unità che si avvia e poi termina con un errore di autorizzazione in journalctl -u moli sta quasi sempre tentando di scrivere in un percorso che ProtectSystem ha appena reso di sola lettura. Sposta quindi quel percorso nella directory dello stato.

MemoryMax=768M rende sicuro eseguire il servizio insieme all'applicazione. L'unità riceve il proprio cgroup. Quando il cgroup supera il limite, il kernel termina un processo al suo interno e lascia invariato il resto del server. Il journal registra l'evento:

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

Interpreta quella riga come un'indicazione per il dimensionamento. Le pagine possono richiedere più memoria del previsto oppure il limite può essere troppo basso. Imposta il valore sulla base di una misurazione delle tue pagine, descritta nella sezione successiva. Gli stessi flag di accounting limitano anche qualsiasi altro servizio sul server e limitare memoria e CPU con systemd funziona anche per gli altri servizi.

Misurare direttamente il picco di memoria

I valori pubblicati provengono dall'hardware e dalle pagine di qualcun altro. Il picco di memoria determina se il server resta operativo e dipende interamente dai contenuti che carichi. Misura il consumo prima di dimensionare le risorse.

Per un'acquisizione singola, usa il binario time, che restituisce molte più informazioni rispetto al builtin della shell con lo stesso nome:

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

L'output termina con un blocco di statistiche sulle risorse che include Maximum resident set size (kbytes). Dividilo per 1024 per ottenere i MiB. Eseguilo su dieci pagine visitate realmente dal tuo agent, non su example.com, e conserva il risultato peggiore invece della media, perché l'OOM killer reagisce ai picchi.

Per il servizio, leggi il contatore che il kernel mantiene già per il relativo cgroup:

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

memory.peak è un conteggio in byte e rappresenta il valore massimo raggiunto dall'ultimo avvio dell'unità, quindi un riavvio lo azzera. Questo valore deve essere inferiore a MemoryMax, lasciando ulteriore margine per la pagina più pesante che non hai ancora visitato. systemd-cgtop -m mostra l'utilizzo corrente per unità ed è il modo più rapido per individuare quale servizio del server sta consumando più risorse in quel momento.

Dove non funziona Moli e quando serve ancora Chrome?

Il progetto esegue anche un benchmark su 1,308 attività comparabili di automazione del browser e pubblica il punteggio di diversi engine.

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
  }
]

Tra questi 5 engine, Moli 0.1.1 ha completato il 81.88 percento delle attività, mentre Chrome, l'engine di riferimento, ne ha completato il 99.85 percento. Il progetto valuta se stesso sulla propria suite, quindi questo dato va considerato un'affermazione del progetto e non un risultato indipendente.

L'interpretazione pratica è semplice. Circa una attività su cinque che Chrome ha completato non è riuscita con Moli. Se l'agente visita un insieme fisso di pagine sotto il tuo controllo, questo rapporto dice poco, perché le tue pagine funzionano oppure non funzionano e puoi verificarlo già oggi. Se l'agente naviga sul web pubblico, si tratta di un tasso reale di errore che devi considerare nella progettazione.

I casi di errore sono prevedibili in base all'ambito dichiarato dal progetto.

  • Applicazioni che disegnano l'interfaccia in un elemento Canvas invece che nel DOM, perché la fedeltà di Canvas è esplicitamente fuori ambito
  • Qualsiasi applicazione che richieda WebGL o il compositing tramite GPU, perché non esiste un compositore GPU
  • Video protetti da DRM e riproduzione di contenuti multimediali complessi
  • Test visivi che verificano screenshot identici a livello di pixel rispetto a Chrome, perché la compatibilità con Chrome non è un obiettivo

L'altro dato citato dal progetto, ovvero il superamento di una esecuzione completa con 1.612 milioni di test della piattaforma web, riguarda la copertura degli standard. Non garantisce il funzionamento sui siti che l'agente visiterà. Una pagina può usare esclusivamente standard ben supportati e fallire comunque un controllo anti-bot; inoltre, nessun punteggio dell'engine copre questo aspetto.

Devi quindi mantenere il fallback nella progettazione. Invia prima ogni URL a Moli. Quando una pagina restituisce contenuto vuoto o un selettore non compare mai, riprova con quell'URL usando Playwright per pilotare Chrome reale, su una macchina più potente o secondo una pianificazione in cui un processo da 773 MiB sia sostenibile. La maggior parte degli agenti trascorre la maggior parte del tempo su pagine ordinarie, quindi l'engine leggero gestisce il volume e quello costoso gestisce i casi rari.

FAQ

Moli può sostituire Chrome headless per il mio agente?

Per leggere le pagine, estrarre testo ed eseguire clic ordinari, in genere sì. Nel benchmark del progetto, su 1,308 attività, ha completato il 81.88 percento rispetto al 99.85 percento di Chrome; quindi circa un'attività su cinque richiede una funzione che Moli non supporta. Le lacune note riguardano le applicazioni renderizzate su canvas, WebGL e i video DRM. Indirizza questi URL a Chrome reale invece di ripristinare Chrome per tutte le richieste.

Quanta RAM richiede Moli su un VPS?

Il progetto indica un RSS mediano di 73 MiB durante una scansione di 192 URL e un PSS massimo di 102.46 MiB in un episodio di esempio dell'agente, rispetto a un valore mediano di 773 MiB per Chrome headless. Questi sono i valori riportati dal progetto sulle proprie pagine. Misura il tuo ambiente con /usr/bin/time -v attorno a una chiamata moli fetch per l'uso una tantum, oppure leggi /sys/fs/cgroup/system.slice/moli.service/memory.peak per il servizio, quindi imposta MemoryMax al di sopra del valore massimo osservato.

È sicuro esporre la porta 9222 su Internet?

No. CDP non dispone di autenticazione, quindi chiunque possa raggiungere quella porta può controllare il browser e leggere qualsiasi contenuto accessibile dal browser. Mantieni --host 127.0.0.1 e raggiungi l'endpoint da un altro computer tramite un tunnel SSH o un'interfaccia VPN privata. Se devi eseguire il bind su un altro indirizzo, usa un'interfaccia privata e controlla l'accesso con il firewall.

Perché lo screenshot è vuoto oppure il clic non viene eseguito su nulla?

Il layout è disabilitato per impostazione predefinita. Il README indica come criterio predefinito LayoutPolicy::Mock, quindi la geometria degli elementi non è reale e tutto ciò che dipende da un riquadro nella pagina non ha dati utilizzabili. Avvia il server con moli serve --layout oppure aggiungi --layout a moli fetch: lo screenshot e i percorsi basati sulle coordinate inizieranno a funzionare. Le immagini mancanti dipendono da un flag diverso: --resource.

Quale versione di Moli devo installare?

Fissa una versione e registrala. Ad agosto 2026, la release corrente è 1.0.1, mentre i dati del benchmark pubblicati dal progetto sono stati misurati sulla versione 0.1.1; le due versioni non sono quindi intercambiabili quando confronti i risultati con un'altra persona. Scarica il moli-x86_64-unknown-linux-gnu.tar.gz di quel tag e installa personalmente il binario invece di affidarti allo shell installer, che risolve la release più recente anziché il tag da cui lo hai scaricato; quindi verifica con moli --version.