Cài Moli: headless browser nhẹ cho agent trên VPS
Headless Chrome ngốn tài nguyên trên VPS nhỏ? Cài Moli, mở CDP trên loopback, trỏ agent vào đó và biết rõ giới hạn khi thiếu Canvas, GPU hoặc media.
Một headless browser phù hợp với VPS nhỏ
Moli là headless browser dành cho AI agent. Nó đủ nhỏ để self-host trên VPS mà headless Chrome không thể chạy vừa. Đây là browser engine được viết bằng Rust, không phải wrapper quanh Chromium. Nó hỗ trợ Chrome DevTools Protocol (CDP), là protocol mà thư viện automation của bạn đã sử dụng. Bạn chỉ cần cài một binary, chạy moli serve, rồi trỏ Playwright hoặc code agent của bạn đến http://127.0.0.1:9222.
Hãy đọc các giới hạn trước khi cài. Project nêu rõ phạm vi của mình: không có GUI browser, không có GPU compositor, không có độ tương thích từng pixel với Chrome, và không hỗ trợ Canvas hoặc phát media với độ trung thực cao. Các trang cần những tính năng này sẽ fail. Chrome thực chạy qua Playwright vẫn là phương án fallback. Phần cuối cho biết cách xác định trang nào cần đến nó.
Mọi command bên dưới đều lấy từ README của project và các skill file đã publish, được kiểm tra vào tháng 8 năm 2026. Mọi con số trong các biểu đồ đều là số liệu project công bố cho engine của mình, không phải số đo từ site này. Chú thích của từng biểu đồ đều nêu rõ điều đó. Nếu bạn vẫn đang chọn engine, bài khảo sát rộng hơn về headless browser cho agent trên VPS trình bày các lựa chọn thay thế.
Vì sao Chrome headless dùng nhiều memory?
Chrome là trình duyệt đa tiến trình. Mỗi tab và mỗi iframe khác site có một renderer process riêng. Mỗi renderer có V8 heap và graphics buffer riêng. Thiết kế này phù hợp với desktop, vì một tab bị crash không làm toàn bộ cửa sổ dừng theo. Trên VPS 2 GB, một bước browse có thể dùng nhiều memory hơn chính application bạn đang chạy.
Project đã crawl 192 URL public hỗn hợp bằng 4 engine và công bố kết quả.
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 trả về 101 trang hữu ích, còn Moli trả về 103, nên trong mẫu này, 2 engine đọc được tỷ lệ website gần như tương đương. Khác biệt nằm ở memory: RSS median (resident set size, lượng memory mà process thực sự giữ trong RAM) là 773 MiB với Chrome, so với 73 MiB với Moli. Xu hướng này có cơ sở vì nó xuất phát từ kiến trúc đa tiến trình. Không nên mặc định rằng tỷ lệ chính xác trên các page của bạn cũng giống vậy.
Median không phải con số gây sự cố. Peak mới là vấn đề. Khi một máy 2 GB hết memory, kernel sẽ chọn một process để kill. Bản ghi sẽ xuất hiện trong dmesg -T hoặc 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:0Agent của bạn không bao giờ thấy dòng đó. Agent chỉ thấy browser đã ngừng phản hồi, thường dưới dạng lỗi Playwright như page.goto: Page crashed hoặc một target đã đóng. Lỗi này không đề cập đến memory. Vì vậy, khi agent lỗi ngẫu nhiên trên máy nhỏ, việc đầu tiên cần kiểm tra là OOM (out of memory) killer. Tính toán tài nguyên cho peak cũng chính là việc chọn RAM và CPU cho một agent VPS.
Cài binary Moli với một version cố định
Project phát hành shell installer và các tarball dựng sẵn trên GitHub releases. Tính đến tháng 8 năm 2026, release hiện tại là 1.0.1, được phát hành vào ngày 18 tháng 8 năm 2026. Các số liệu benchmark được trích trong hướng dẫn này do project đo trên version 0.1.1, vì vậy hãy xem chúng là đặc điểm khái quát của engine, không phải cam kết về build bạn cài.
Hãy cố định version. Một installer luôn resolve latest sẽ chuyển agent của bạn sang browser engine khác ở lần rebuild tiếp theo. Thay đổi trong hành vi của browser là loại thay đổi bạn nên lên lịch trước, không phải đến lúc gặp mới phát hiện.
Shell installer là cách nhanh để bắt đầu. Bạn nên đọc nó trước khi chạy.
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.shHãy đọc script trước khi chạy. Script này ngắn. Nó chọn một archive từ uname -m, rồi giải nén một binary duy nhất vào ~/.local/bin. Trên x86_64, nó dùng moli-x86_64-unknown-linux-gnu.tar.gz. Trên server Arm, nó dùng archive aarch64, nên cả gói VPS Arm và x86 đều được hỗ trợ. Đặt MOLI_INSTALL_DIR để cài vào vị trí khác. Hãy chú ý version mà script resolve: đó là release mới nhất, không phải tag mà bạn lấy script về từ đó. Cách này phù hợp để xem thử lần đầu, nhưng không phù hợp với một rebuild cần có kết quả lặp lại.
Vì vậy, với mọi cài đặt lâu dài, hãy tự thực hiện các bước installer làm và tự chỉ rõ archive chính xác. Đây cũng là cách đặt binary vào vị trí mà system service có thể truy cập, đồng thời tránh phải pipe một script đã tải vào 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 --versionmoli --version in version bạn đã cố định là toàn bộ bước kiểm tra. moli: command not found ngay sau installer cho biết thư mục cài đặt không nằm trong PATH của bạn, và installer sẽ in một dòng cho biết thư mục bạn cần thêm.
Trích xuất một lần bằng moli fetch
Nhiều job mà agent giao cho browser là “tải URL này và cho tôi biết nội dung”. Việc đó không cần server. moli fetch khởi động engine, tải một trang, ghi một artifact vào standard output rồi thoát, nên không giữ memory giữa các lần gọi.
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.jsonLệnh đầu tiên in trang dưới dạng Markdown, bắt đầu bằng # Example Domain. Markdown là định dạng nhẹ nhất để chuyển cho model vì loại bỏ markup và giữ lại phần văn bản. semantic_tree_text giữ lại vai trò và cấu trúc. Đây là lựa chọn phù hợp cho các trang phụ thuộc nhiều vào navigation, nơi các liên kết quan trọng không kém phần nội dung. --dump json kèm HTTP status và request trace. Hãy dùng nó khi fetch trả về rỗng và bạn cần biết nguyên nhân.
Chiến lược chờ quyết định bạn nhận được nội dung hay một shell rỗng. --wait-until networkidle trả về khi network trở nên yên. --wait-until domstable trả về khi DOM ngừng thay đổi. Đây là lựa chọn tốt hơn cho trang liên tục polling ở background và vì thế không bao giờ thật sự yên. --wait-selector chờ một selector do bạn chỉ định. Đây là chiến lược duy nhất có thông tin về trang đang fetch, nên đáng tin cậy nhất khi bạn biết target.
Screenshot và PDF cần layout thực. Layout mặc định bị tắt:
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.pdfREADME gọi policy layout mặc định là LayoutPolicy::Mock: geometry được giả lập và không có gì được paint, vì layout và paint là phần tốn tài nguyên nhất của browser. Đây là lý do các con số memory ở trên có kết quả như vậy. Điều đó cũng có nghĩa là một PNG rỗng thường là do thiếu flag --layout, không phải do trang bị lỗi.
Với các URL do agent tìm thấy thay vì URL do bạn chọn, hãy thêm --block-private-networks. Agent có thể theo các liên kết đọc được trên một trang và bị điều hướng đến http://169.254.169.254/ để lấy credential của cloud instance, hoặc đến một database port trên localhost vốn chưa bao giờ được phép public ra web. Flag này từ chối navigation vào private address space, còn --block-cidrs giới hạn chặt hơn nữa. Khi job là crawling thay vì đọc một trang, cấu trúc pipeline đó được trình bày trong các lựa chọn thay thế Firecrawl tự host, còn bước trước đó là tìm URL, được trình bày trong skill tìm kiếm cho agent dùng SearXNG làm backend.
Trỏ agent vào Moli qua CDP
Với agent phải điều hướng và click qua nhiều bước, hãy chạy server riêng.
moli serve --host 127.0.0.1 --port 9222127.0.0.1 và port 9222 là các giá trị mặc định, nên moli serve không kèm tham số đã chỉ bind vào loopback. Tuy vậy, hãy ghi rõ cả hai trong mọi cấu hình lâu dài, vì người đọc service file sau này không phải nhớ giá trị mặc định là gì.
Hãy kiểm tra server trước khi kết nối client vào đó:
curl -s http://127.0.0.1:9222/json/versionServer hoạt động bình thường sẽ trả về một JSON object có field webSocketDebuggerUrl. Client CDP sẽ attach vào URL đó. curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused nghĩa là không có tiến trình nào đang listen, vì vậy hãy xem terminal nơi bạn đã khởi động server, hoặc dùng journalctl -u moli -n 50 nếu server đã chạy dưới dạng service. /json/list liệt kê các target đang mở. /json/protocol liệt kê các domain mà build này triển khai. Đây là cách kiểm tra một CDP method mà bạn phụ thuộc có tồn tại ở đây hay không.
Playwright attach vào endpoint đó thay vì tự khởi động browser:
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();Dòng quan trọng là connectOverCDP, không phải chromium.launch(). Ở đây không có child Chromium process, nên executablePath và các container flag thông dụng như --no-sandbox không có tác dụng. Proxy, cookie và user-agent cũng phải cấu hình bằng flag riêng của Moli server vì cùng lý do đó. Hãy kỳ vọng CDP chỉ được hỗ trợ một phần, không phải toàn bộ Chrome protocol. Lỗi unsupported-method rõ ràng cho biết giới hạn của engine, không phải bug trong code của bạn.
Hai server flag quyết định agent có thể làm gì. --layout bật geometry thực, cần thiết cho coordinate click và screenshot. --resource tải thêm images, fonts và media tùy chọn. Tùy chọn này tiêu tốn bandwidth và memory trong mỗi lần page load, vì vậy hãy tắt nó cho đến khi một page cho thấy mình cần các tài nguyên đó. --profile-dir lưu lại cookie và storage giữa các lần chạy. Nếu không bật, mọi lần chạy đều không giữ lại trạng thái.
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
}
]Trong workload agent mẫu của project, Moli nhận CDP connection sau 34.85 ms, so với 169.37 ms của Chromium, với peak PSS (proportional set size, trong đó memory của các shared page được chia giữa những process cùng sử dụng chúng) là 102.46 MiB, so với 348.82 MiB. Khác biệt về cấu trúc nằm ở cột cuối: 1 process so với 11. Một process là một đối tượng để systemd supervise và một cgroup để giới hạn tài nguyên. Đây là lý do section tiếp theo ngắn gọn.
Chạy moli serve dưới dạng systemd service trên loopback
Chạy server dưới dạng service khi agent cần một browser luôn sẵn sàng để sử dụng. Khi không cần, hãy tiếp tục dùng moli fetch cho từng URL, vì server ở trạng thái idle vẫn chiếm bộ nhớ.
Không bind port 9222 vào interface public. CDP không có bất kỳ bước authentication nào. Bất kỳ ai truy cập được port đó đều có thể điều khiển browser và đọc mọi thứ mà browser truy cập được, bao gồm cả cookie trong thư mục profile của bạn. Giữ port này trên 127.0.0.1. Từ máy khác, hãy truy cập qua SSH tunnel (ssh -L 9222:127.0.0.1:9222 user@your-vps) hoặc qua private VPN interface, rồi để agent kết nối đến http://127.0.0.1:9222 ở phía bên kia của tunnel.
Tạo service user, sau đó tạo unit file:
sudo useradd --system --home-dir /var/lib/moli --shell /usr/sbin/nologin moliGhi nội dung sau vào /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 phải hiển thị active (running), còn curl phải trả về discovery JSON. ProtectSystem=strict mount toàn bộ filesystem ở chế độ read-only cho unit này. Vì vậy StateDirectory=moli không phải tùy chọn trong trường hợp này: nó tạo /var/lib/moli, gán quyền sở hữu cho service user và cho phép ghi tại đúng path đó. Một unit khởi động rồi dừng với lỗi permission trong journalctl -u moli gần như luôn đang cố ghi vào nơi mà ProtectSystem vừa chuyển sang read-only, vì vậy hãy chuyển path đó vào state directory.
MemoryMax=768M giúp chạy service này an toàn cùng với application của bạn. Unit có cgroup riêng. Khi cgroup vượt quá giới hạn, kernel sẽ kill một tiến trình bên trong cgroup đó và để các phần còn lại của máy hoạt động bình thường. journal sẽ ghi lại sự kiện này:
moli.service: A process of this unit has been killed by the OOM killer.Hãy xem dòng này như tín hiệu để điều chỉnh kích thước. Các page có thể nặng hơn dự kiến hoặc giới hạn đang quá thấp. Đặt con số dựa trên kết quả đo từ chính các page của bạn; phần tiếp theo sẽ trình bày cách đo. Các flag accounting tương tự cũng giới hạn mọi service khác trên máy, và giới hạn memory và CPU bằng systemd áp dụng cho các service còn lại.
Tự đo memory đỉnh
Các số liệu đã công bố được lấy từ phần cứng và các trang của người khác. Memory đỉnh quyết định máy chủ của bạn có hoạt động ổn định hay không, và giá trị này phụ thuộc hoàn toàn vào nội dung bạn load. Hãy đo trước khi chọn cấu hình.
Để fetch từng lần, dùng binary time. Nó báo cáo nhiều thông tin hơn shell builtin cùng tên:
sudo apt update && sudo apt install -y time
/usr/bin/time -v moli fetch --dump markdown --wait-until networkidle https://example.com > /dev/nullOutput kết thúc bằng một block thống kê resource, trong đó có Maximum resident set size (kbytes). Chia giá trị này cho 1024 để đổi sang MiB. Chạy lệnh trên 10 trang mà agent của bạn thực sự truy cập, không chạy trên example.com. Giữ lại kết quả lớn nhất thay vì giá trị trung bình, vì OOM killer phản ứng với các đỉnh memory.
Đối với service, đọc counter mà kernel đã duy trì cho cgroup của service:
cat /sys/fs/cgroup/system.slice/moli.service/memory.peak
systemd-cgtop -mmemory.peak là số byte, đồng thời là high-water mark kể từ lần unit khởi động gần nhất, nên restart sẽ reset giá trị này. Đây là mức mà MemoryMax phải cao hơn, đồng thời cần chừa thêm khoảng trống cho trang nặng nhất mà bạn chưa truy cập. systemd-cgtop -m hiển thị mức sử dụng hiện tại theo từng unit. Đây là cách nhanh nhất để xác định service nào trên máy đang tốn nhiều resource nhất.
Moli gặp lỗi ở đâu, và khi nào vẫn cần Chrome?
Dự án cũng chạy benchmark gồm 1,308 tác vụ browser automation tương đương và công bố điểm số của một số engine.
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
}
]Trong số 5 engine đó, Moli 0.1.1 hoàn thành 81.88 phần trăm số tác vụ, còn Chrome, engine tham chiếu, hoàn thành 99.85 phần trăm. Dự án tự chấm điểm trên bộ test của chính mình, nên hãy xem đây là một tuyên bố, không phải kết quả độc lập.
Cách diễn giải thực tế khá đơn giản. Khoảng 1 trong 5 tác vụ mà Chrome hoàn thành đã thất bại trên Moli. Nếu agent của bạn chỉ truy cập một tập trang cố định do bạn kiểm soát, tỷ lệ này không cho biết nhiều, vì các trang đó hoặc hoạt động hoặc không, và bạn có thể kiểm tra ngay trong hôm nay. Nếu agent duyệt web mở, đây là tỷ lệ lỗi thực tế mà bạn phải tính đến trong thiết kế.
Những trường hợp lỗi có thể dự đoán từ phạm vi mà dự án công bố.
- Các ứng dụng vẽ giao diện trực tiếp vào phần tử Canvas thay vì DOM, vì Canvas fidelity nằm ngoài phạm vi hỗ trợ
- Mọi thứ cần WebGL hoặc GPU compositing, vì không có GPU compositor
- Video được bảo vệ bằng DRM và các tác vụ phát media nặng
- Các bài test trực quan yêu cầu screenshot khớp chính xác từng pixel với Chrome, vì tương thích tuyệt đối với Chrome không phải mục tiêu
Con số còn lại mà dự án nêu, tức một lần chạy hoàn chỉnh vượt qua 1.612 triệu web platform test, nói về mức độ hỗ trợ standards. Nó không phải cam kết về các website mà agent của bạn sẽ truy cập. Một trang có thể chỉ dùng các standard được hỗ trợ tốt nhưng vẫn thất bại ở bước bot check, và không có điểm số engine nào bao quát được trường hợp đó.
Vì vậy, hãy giữ fallback trong thiết kế. Gửi mọi URL đến Moli trước. Khi một trang trả về nội dung rỗng hoặc một selector không bao giờ xuất hiện, hãy thử lại URL đó bằng Playwright điều khiển Chrome thật, trên máy lớn hơn hoặc chạy theo lịch vào thời điểm một process 773 MiB là có thể chấp nhận. Hầu hết agent dành phần lớn thời gian cho các trang thông thường, nên engine nhỏ xử lý phần lớn lưu lượng, còn engine đắt tài nguyên xử lý các trường hợp còn lại.
FAQ
Moli có thể thay thế Chrome headless cho agent của tôi không?
Để đọc trang, trích xuất văn bản và thực hiện các thao tác click thông thường thì thường là có. Trong benchmark riêng của dự án trên 1,308 tác vụ, Moli hoàn thành 81.88 phần trăm, còn Chrome đạt 99.85 phần trăm. Như vậy, khoảng 1 trên 5 tác vụ cần một khả năng mà Moli không hỗ trợ. Ứng dụng render bằng Canvas, WebGL và video DRM là những điểm còn thiếu đã biết. Hãy định tuyến các URL đó đến Chrome thật thay vì chuyển toàn bộ trở lại Chrome.
Moli cần bao nhiêu RAM trên VPS?
Dự án báo cáo RSS trung vị là 73 MiB trong một lần crawl 192 URL, và PSS cao nhất là 102.46 MiB trong một episode mẫu của agent. Con số median của headless Chrome là 773 MiB. Đây là số liệu của họ trên các trang của họ. Hãy đo hệ thống của bạn bằng /usr/bin/time -v quanh một lần gọi moli fetch khi chạy one-shot, hoặc đọc /sys/fs/cgroup/system.slice/moli.service/memory.peak đối với service, rồi đặt MemoryMax cao hơn con số lớn nhất bạn thấy.
Có an toàn không nếu expose port 9222 ra Internet?
Không. CDP không có authentication, nên bất kỳ ai truy cập được port đó đều có thể điều khiển browser của bạn và đọc mọi nội dung mà browser có thể truy cập. Hãy giữ --host 127.0.0.1, và truy cập endpoint từ máy khác thông qua SSH tunnel hoặc qua private VPN interface. Nếu bắt buộc phải bind vào địa chỉ khác, hãy đặt nó trên private interface và kiểm soát quyền truy cập bằng firewall.
Tại sao screenshot của tôi trống, hoặc click không trúng gì?
Layout mặc định bị tắt. README gọi policy mặc định là LayoutPolicy::Mock, nên geometry của element không phản ánh thực tế và mọi thao tác phụ thuộc vào box trên trang đều không có dữ liệu để hoạt động. Hãy khởi động server bằng moli serve --layout, hoặc thêm --layout vào moli fetch, rồi screenshot và các thao tác theo tọa độ sẽ bắt đầu hoạt động. Ảnh bị thiếu là một flag khác: --resource.
Tôi nên cài phiên bản Moli nào?
Hãy cố định một phiên bản và ghi lại phiên bản đó. Tính đến tháng 8 năm 2026, release hiện tại là 1.0.1, còn các số liệu benchmark được dự án công bố được đo trên 0.1.1. Vì vậy, hai phiên bản này không thể dùng thay thế cho nhau khi bạn đối chiếu với người khác. Hãy tải moli-x86_64-unknown-linux-gnu.tar.gz của tag đó và tự cài binary thay vì phụ thuộc vào shell installer. Installer này sẽ chọn release mới nhất thay vì tag mà bạn đã fetch, sau đó xác nhận bằng moli --version.