SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-25

Moli 自架:給 AI agent 的輕量無頭瀏覽器

在小型 VPS 上自架 Moli,透過 loopback 提供 CDP,讓 Playwright 或 agent 使用;了解它何時無法取代 headless Chrome,以及記憶體取捨。

適合小型 VPS 的無頭瀏覽器

Moli 是供 AI agent 使用的無頭瀏覽器,體積小,可自行代管在無法容納無頭 Chrome 的 VPS 上。它是以 Rust 撰寫的瀏覽器引擎,不是 Chromium 的封裝程式,並支援 Chrome DevTools Protocol (CDP),也就是現有自動化程式庫使用的通訊協定。安裝一個執行檔後,執行 moli serve,再將 Playwright 或自有的 agent 程式碼指向 http://127.0.0.1:9222

安裝前,請先了解取捨。該專案明確說明其適用範圍:沒有 GUI 瀏覽器、沒有 GPU compositor、不保證與 Chrome 逐像素一致,也不支援高保真 Canvas 或媒體播放。需要這些功能的頁面會失敗。在 Playwright 中使用真正的 Chrome 仍是備援方案,最後一節會說明如何判斷哪些頁面需要它。

以下每個指令都來自該專案的 README 與已發布的 skill 檔案,並於 2026 年 8 月檢查。圖表中的每個數字,都是該專案發布的自家引擎數據,不是本網站測得的結果;每個圖表說明都會標示這一點。如果你仍在選擇引擎,較完整的 VPS 上供 agent 使用的無頭瀏覽器比較了其他方案。

為什麼 headless Chrome 會使用這麼多記憶體?

Chrome 是多程序瀏覽器。每個分頁與每個跨網站 iframe 都會取得自己的 renderer process,而每個 renderer 都包含自己的 V8 heap 與 graphics buffers。這種設計適合桌面環境,因為單一分頁當機時,不會連帶使整個視窗停止運作。但在 2 GB VPS 上,單一瀏覽步驟所需的記憶體,可能比實際執行的應用程式還多。

該專案使用四個引擎,對 192 個混合型公開 URL 進行爬取,並公布結果。

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 回傳 101 個有效頁面,Moli 回傳 103 個,因此在這組樣本中,兩個引擎讀取的網頁比例大致相同。兩者的差異在記憶體:Chrome 的 RSS 中位數(resident set size,程序實際持有的 RAM)為 773 MiB,Moli 則為 73 MiB。這個方向具有可信度,因為它符合程序架構所導致的結果。但不應假設你的頁面也會有完全相同的比例。

真正造成問題的不是中位數,而是峰值。當 2 GB 主機耗盡記憶體時,kernel 會選擇一個程序並將其終止,記錄會出現在 dmesg -Tjournalctl -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

你的 agent 不會看到那行記錄。它只會看到停止回應的瀏覽器,通常會收到類似 page.goto: Page crashed 的 Playwright 錯誤,或看到 target 已關閉。這類錯誤不會提到記憶體,因此當 agent 在小型主機上隨機失敗時,首先應檢查 OOM(out of memory)killer。為峰值配置資源,與 為 agent VPS 選擇 RAM 與 CPU 是相同的工作。

安裝 Moli 二進位檔,固定使用單一版本

該專案會在 GitHub releases 提供 shell installer 和預先建置的 tarball。截至 2026 年 8 月,目前版本為 1.0.1,發布日期為 2026 年 8 月 18 日。本指南引用的基準數據是該專案以 0.1.1 測得,因此應將其視為引擎的大致特性,不要視為你所安裝版本的保證。

請固定版本。若 installer 每次都解析 latest,下一次重新建置時就會將你的 agent 移至不同的瀏覽器引擎。瀏覽器行為的變更應安排後再導入,而不是在建置時才發現。

shell installer 是快速開始的方法,但執行前應先閱讀內容。

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

執行前先閱讀該 script。內容很短。它會從 uname -m 選取 archive,然後將單一二進位檔解壓縮至 ~/.local/bin。在 x86_64 上,它會使用 moli-x86_64-unknown-linux-gnu.tar.gz;在 Arm 伺服器上,則會使用 aarch64 archive,因此 Arm 和 x86 VPS 方案 都適用。設定 MOLI_INSTALL_DIR 可安裝至其他位置。請注意它解析的版本:是最新 release,而不是你取得該 script 時所使用的 tag。初步查看時這樣沒有問題,但若要進行可重複的重新建置,這樣就不合適。

因此,對於任何永久安裝,請手動執行 installer 的工作,並自行指定確切的 archive。這也是將二進位檔放到 system service 可存取位置的方法,同時可避免將下載的 script 透過 pipe 傳給 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 輸出你固定的版本,就是完整的檢查方式。在 installer 後直接執行 moli: command not found,表示安裝目錄不在你的 PATH 中;installer 會輸出一行訊息,指出你需要加入的目錄。

使用 moli fetch 進行單次擷取

代理程式交給瀏覽器的許多工作都是「載入這個 URL,告訴我內容」。這類工作完全不需要伺服器。moli fetch 會啟動引擎、載入單一頁面、將單一產物寫入標準輸出後結束,因此呼叫之間不會保留記憶體。

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

第一個選項會將頁面輸出為 Markdown,並以 # Example Domain 開頭。Markdown 最適合提供給模型,因為它會移除標記並保留文字。semantic_tree_text 會保留角色與結構。對於連結和正文同樣重要的導覽密集頁面,這是較合適的選擇。--dump json 會帶上 HTTP 狀態與請求追蹤資訊。擷取結果為空時,可使用此選項了解原因。

等待策略會決定你取得的是內容還是空殼。--wait-until networkidle 會在網路活動停止後返回。--wait-until domstable 會在 DOM 停止變更後返回。對於持續在背景輪詢、因此永遠不會完全停止網路活動的頁面,這是較好的選擇。--wait-selector 會等待你指定的單一 selector。這是唯一了解所擷取頁面內容的策略。只要知道目標,它通常也是最可靠的選擇。

螢幕擷取畫面與 PDF 需要實際版面配置,而版面配置預設為停用:

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

README 將預設版面配置政策命名為 LayoutPolicy::Mock:系統會模擬幾何資訊,但不進行繪製,因為版面配置與繪製是瀏覽器最耗費資源的部分。這項預設值正是上述記憶體數據呈現該結果的原因。這也表示空白的 PNG 通常是缺少 --layout flag,而不是頁面損壞。

對於代理程式找到而不是你自行選定的 URL,請加入 --block-private-networks。代理程式若依據頁面讀取的連結繼續導覽,可能會被引導去擷取 http://169.254.169.254/ 的雲端執行個體憑證,或 localhost 上原本不應暴露於網際網路的資料庫連接埠。這個 flag 會拒絕導覽至私有位址空間,而 --block-cidrs 會進一步縮小範圍。工作若是爬取多個頁面而不是讀取單一頁面,自架 Firecrawl 替代方案說明了這類 pipeline 的架構;而更前一步、也就是尋找 URL,本文件則介紹以 SearXNG 為基礎的代理程式搜尋 skill

讓 agent 透過 CDP 連線至 Moli

如果 agent 需要執行多個步驟來瀏覽及點選,請改為執行伺服器。

moli serve --host 127.0.0.1 --port 9222

127.0.0.1 和連接埠 9222 是預設值,因此不帶參數的 moli serve 已只繫結至 loopback。不過,在任何永久設定中仍應明確寫出這兩項,讓下次閱讀服務檔案的人不必記住預設值。

將用戶端連線至伺服器前,先確認伺服器狀態:

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

正常的伺服器會回應包含 webSocketDebuggerUrl 欄位的 JSON 物件,而該 URL 就是 CDP 用戶端附加的端點。curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused 表示沒有程序正在監聽,因此請查看啟動伺服器的終端機;如果伺服器已設為服務,則執行 journalctl -u moli -n 50/json/list 會列出開啟的 target,/json/protocol 會列出此版本實作的網域。藉此即可確認所依賴的 CDP 方法是否存在。

Playwright 會附加至該端點,而不是自行啟動瀏覽器:

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();

關鍵行是 connectOverCDP,不是 chromium.launch()。這裡沒有子 Chromium 程序,因此 executablePath 及一般容器旗標(例如 --no-sandbox)都沒有可作用的對象。Proxy、cookie 和 user-agent 設定也應透過 Moli 伺服器本身的旗標傳入,原因相同。請預期只有部分 CDP 支援,而不是完整的 Chrome protocol。明確回報的 unsupported-method 錯誤表示引擎的功能邊界,不是程式碼中的錯誤。

兩個伺服器旗標會決定 agent 能執行的操作。--layout 會啟用實際幾何資訊,座標點選和螢幕擷取都需要此功能。--resource 會擷取選用的圖片、字型和媒體;每次載入頁面都會增加頻寬和記憶體用量,因此除非頁面證明需要,否則請維持關閉。--profile-dir 會在執行期間保留 cookie 和儲存資料;未啟用時,每次執行都會使用一次性狀態。

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

在專案的範例 agent 工作負載中,Moli 建立 CDP 連線所需時間為 34.85 ms,而 Chromium 為 169.37 ms;峰值 PSS(proportional set size,會將共用頁面分攤至共用該頁面的程序並計入記憶體)為 102.46 MiB,而 Chromium 為 348.82 MiB。結構上的差異在最後一欄:1 個程序,而 Chromium 為 11 個。單一程序代表 systemd 只需監督一個程序,也只需限制一個 cgroup,因此下一節可以保持簡短。

在 loopback 上將 moli serve 作為 systemd 服務執行

當 agent 需要一個持續等待連線的瀏覽器時,請將伺服器作為服務執行。不需要時,仍應依每個 URL 使用 moli fetch,因為閒置的伺服器仍會占用記憶體。

不要將連接埠 9222 綁定至公開網路介面。CDP 完全沒有任何形式的驗證程序。任何能連到該連接埠的人,都能操控瀏覽器並讀取瀏覽器可存取的內容,包括 profile 目錄中的任何 cookie。請讓服務監聽 127.0.0.1。若要從其他機器存取,請透過 SSH tunnel(ssh -L 9222:127.0.0.1:9222 user@your-vps)或私有 VPN 介面連線,並讓 agent 在 tunnel 的另一端連至 http://127.0.0.1:9222

建立服務使用者,然後建立 unit 檔案:

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

寫入 /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 應顯示 active (running),而 curl 應回傳 discovery JSON。ProtectSystem=strict 會為此 unit 以唯讀方式掛載整個檔案系統,因此 StateDirectory=moli 在此不可省略:它會建立由服務使用者擁有的 /var/lib/moli,並讓該路徑可寫入。若 unit 啟動後隨即因 journalctl -u moli 的權限錯誤結束,幾乎總是因為它嘗試寫入 ProtectSystem 剛設為唯讀的某個位置,因此請將該路徑移至狀態目錄下。

MemoryMax=768M 可確保此服務與應用程式並行執行時的安全性。此 unit 會使用自己的 cgroup;當該 cgroup 超過限制時,kernel 會終止其中的某個程序,其他系統則不受影響。journal 會記錄相關訊息:

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

請將該行視為容量規劃訊號。可能是頁面所需的資源高於預期,也可能是限制值過低。請根據自行測量頁面所得的結果設定數值;下一節會說明測量方式。同一組 accounting flags 也能限制此主機上的其他服務,而 使用 systemd 限制記憶體與 CPU 會套用至其餘服務。

自行測量記憶體峰值

已發布的數據來自他人的硬體與頁面。記憶體峰值決定您的主機能否持續運作,而峰值完全取決於載入的內容。請先測量,再決定規格。

若要執行單次擷取,請使用 time 二進位檔。它提供的資訊遠多於同名的 shell 內建指令:

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

輸出的結尾會列出一組資源統計資料,其中包含 Maximum resident set size (kbytes)。將其除以 1024,即可換算為 MiB。請對代理程式實際造訪的 10 個頁面執行測試,不要測試 example.com,並保留最差結果而非平均值,因為 OOM killer 會根據峰值採取行動。

對於服務,請讀取 kernel 已為其 cgroup 維護的計數器:

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

memory.peak 是位元組數,也是該 unit 上次啟動以來的最高水位標記,因此重新啟動後會重設。這個數值就是 MemoryMax 必須高於的基準,並且還要為尚未造訪過的最繁重頁面預留額外空間。systemd-cgtop -m 會顯示每個 unit 的即時使用量,是目前最快找出主機上哪個服務最耗用資源的方法。

Moli 哪些情況會失敗?什麼時候仍需要 Chrome?

該專案也會對 1,308 個可比較的瀏覽器自動化工作執行基準測試,並公布數個引擎的分數。

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

在這些 5 個引擎中,Moli 0.1.1 完成了 81.88% 的工作,而作為參考引擎的 Chrome 完成了 99.85%。這是專案使用自身測試套件評分的結果,因此應將其視為專案聲稱,而非獨立測試結果。

實務上的解讀很簡單。Moli 約有 5 個工作中的 1 個失敗,而 Chrome 可以完成該工作。如果你的 agent 只會瀏覽你所控制的固定網頁集合,這個比例幾乎沒有參考價值,因為這些網頁不是正常運作,就是無法運作,而且你可以在今天下午確認結果。如果你的 agent 會瀏覽公開網路,這就是必須在設計中處理的實際失敗率。

根據專案所述的適用範圍,可以預測會失敗的情況。

  • 將介面繪製到 Canvas 元素,而不是 DOM 的應用程式,因為 Canvas fidelity 明確不在適用範圍內
  • 需要 WebGL 或 GPU compositing 的任何內容,因為沒有 GPU compositor
  • 受 DRM 保護的影片,以及要求較高的媒體播放
  • 對照 Chrome 執行 pixel-exact screenshot 比對的視覺測試,因為與 Chrome 保持 parity 並不是目標

專案引用的另一項數據是:一次完整執行可通過 1.612 million 個 web platform tests。這表示標準支援範圍,不代表你的 agent 將瀏覽的網站一定能正常運作。網頁即使只使用支援良好的標準,仍可能因 bot check 而失敗,而且任何引擎分數都無法涵蓋這種情況。

因此,請在設計中保留 fallback。先將每個 URL 傳送給 Moli。當網頁回傳空白內容,或某個 selector 始終未出現時,再使用 Playwright 驅動真正的 Chrome,對該 URL 重試;可在較大型的機器上執行,或安排在能負擔 773 MiB 程序的時段執行。大多數 agent 的時間都花在一般網頁上,因此由輕量引擎處理主要工作量,再由成本較高的引擎處理少數例外。

FAQ

Moli 能取代代理程式使用的 headless Chrome 嗎?

如果只是讀取頁面、擷取文字及一般點擊,通常可以。在專案自己的 1,308 項任務基準測試中,Moli 的完成率為 81.88%,Chrome 則為 99.85%;也就是約每 5 項任務中有 1 項需要 Moli 不支援的功能。目前已知的缺口包括 Canvas 繪製的應用程式、WebGL 及 DRM 影片。將這些 URL 路由至真正的 Chrome,不要因此把所有項目都切回 Chrome。

Moli 在 VPS 上需要多少 RAM?

專案報告指出,在 192-URL 爬取作業中,RSS 中位數為 73 MiB;在一段範例代理程式執行期間,PSS 峰值為 102.46 MiB。headless Chrome 的 RSS 中位數則為 773 MiB。這些是專案在其頁面上公布的數據。若要執行單次作業,請在 moli fetch 呼叫周圍使用 /usr/bin/time -v 進行測量;若是服務,則讀取 /sys/fs/cgroup/system.slice/moli.service/memory.peak,再將 MemoryMax 設定為高於所見最差數值的值。

將 9222 埠暴露至網際網路安全嗎?

不安全。CDP 沒有驗證機制,因此任何能連到該埠的使用者,都可以控制瀏覽器,並讀取瀏覽器可存取的所有內容。請維持 --host 127.0.0.1,並從另一台機器透過 SSH tunnel 或私有 VPN 介面連線至該端點。如果必須繫結其他位址,請繫結至私有介面,並使用防火牆控管存取權限。

為什麼我的 screenshot 是空白,或點擊落在沒有內容的位置?

預設會關閉版面配置。README 將預設原則列為 LayoutPolicy::Mock,因此元素幾何位置不是真實值,任何依賴頁面方框的功能都無法運作。請使用 moli serve --layout 啟動伺服器,或將 --layout 加入 moli fetch;之後 screenshot 與座標操作就會開始運作。缺少圖片則是另一個 flag:--resource

我應該安裝哪個 Moli 版本?

請固定使用一個版本,並記錄所用版本。截至 2026 年 8 月,目前的 release 為 1.0.1;但專案公布的基準測試數據是在 0.1.1 上測得,因此與他人比較結果時,這兩個版本不可互相比較。請下載該 tag 的 moli-x86_64-unknown-linux-gnu.tar.gz,自行安裝 binary,不要依賴 shell installer;該安裝程式會解析最新 release,而不是你所取得的 tag。最後使用 moli --version 確認版本。