SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-21

小型 VPS 如何自托管 Moli 无头浏览器

在小型 VPS 上安装 Moli,将 CDP 绑定到回环地址并连接 AI 代理。了解它为何比无头 Chrome 更省内存,以及不支持高保真 Canvas 和媒体播放的页面。

适合小型 VPS 的无头浏览器

Moli 是供 AI 代理使用的无头浏览器,体积足够小,可以部署在不适合运行无头 Chrome 的 VPS 上。它是使用 Rust 编写的浏览器引擎,不是 Chromium 的封装,并且支持 Chrome DevTools Protocol(CDP),也就是现有自动化库使用的协议。您只需安装一个二进制文件,运行 moli serve,然后让 Playwright 或您自己的代理代码连接到 http://127.0.0.1:9222

安装前请先了解取舍。项目明确说明了自身的范围:不提供 GUI 浏览器、不提供 GPU 合成、不保证与 Chrome 逐像素一致,也不支持高保真 Canvas 或媒体播放。需要这些功能的页面会失败。在 Playwright 中使用真实 Chrome 仍是后备方案,最后一节将说明如何判断哪些页面需要它。

下面的每条命令都来自项目的 README 和已发布的 skill 文件,这些内容于 August 2026 进行了检查。图表中的所有数字都是项目发布的自身引擎数据,不是本网站测量得到的数据;每个图表标题也都对此进行了说明。如果您仍在选择引擎,可以查看更广泛的 VPS 上供代理使用的无头浏览器 调查,了解其他选项。

为什么无头 Chrome 会占用这么多内存?

Chrome 是多进程浏览器。每个标签页和每个跨站 iframe 都有自己的渲染器进程,每个渲染器都包含独立的 V8 堆和图形缓冲区。对于桌面环境,这种设计是合理的,因为一个标签页崩溃时不应导致整个窗口退出。在 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 主机耗尽内存时,内核会选择一个进程并将其终止,记录会写入 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

你的代理程序看不到这行内容。它看到的是停止响应的浏览器,通常表现为 Playwright 错误,例如 page.goto: Page crashed,或目标已关闭。该错误不会提到内存。因此,当代理程序在小型主机上随机失败时,首先应检查 OOM(out of memory)killer。按峰值为系统配置资源,与为代理程序 VPS 选择 RAM 和 CPU是同一项工作。

安装 Moli 二进制文件并固定版本

该项目在 GitHub releases 中提供 shell 安装程序和预构建 tarball。截至 2026 年 8 月,当前版本为 1.0.1,于 18 August 2026 发布。本指南引用的基准数据由项目使用 0.1.1 测得,因此应将其视为引擎的大致表现,而不是对您所安装版本的保证。

请固定版本。始终解析 latest 的安装程序会在下一次重建时将您的代理切换到不同的浏览器引擎,而浏览器行为变化应当由您主动安排,而不是在重建后才发现。

shell 安装程序是最快的安装方式,但在运行前应先阅读它。

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

运行前先阅读脚本。脚本很短。它从您的 uname -m 中选择一个归档文件,然后将其中的单个二进制文件解压到 ~/.local/bin。在 x86_64 上,它选择 moli-x86_64-unknown-linux-gnu.tar.gz;在 Arm 服务器上,它选择 aarch64 归档,因此 Arm 和 x86 VPS 方案 都适用。设置 MOLI_INSTALL_DIR 可将其安装到其他位置。注意它解析的版本:它选择最新版本,而不是您获取脚本时所对应的标签。初步试用这样做没有问题,但对于需要可重复的重建,这种方式不正确。

因此,对于任何永久安装,都应手动执行安装程序的操作,并自行指定确切的归档文件。这样还可以将二进制文件放到系统服务能够访问的位置,并避免将下载的脚本通过管道传给 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 打印您固定的版本即可完成检查。安装程序运行后立即执行 moli: command not found,表示安装目录不在您的 PATH 中;安装程序会打印一行信息,指出需要添加的目录。

使用 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 等待指定的选择器出现。它是唯一了解所提取页面内容的策略。因此,只要知道目标,就应优先使用它。

截图和 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 标志,而不是页面损坏。

对于代理发现的 URL,而不是你主动选择的 URL,应添加 --block-private-networks。代理如果跟随页面中读取到的链接,可能会被引导去提取 http://169.254.169.254/ 中的云实例凭据,或访问 localhost 上原本不应暴露到 Web 的数据库端口。该标志会拒绝导航到私有地址空间,而 --block-cidrs 会进一步缩小允许的范围。如果任务是爬取而不是读取单个页面,自托管 Firecrawl 替代方案介绍了这类流水线的结构;而在此之前,如何发现 URL,则见基于 SearXNG 的代理搜索技能

通过 CDP 为 agent 连接 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 对象。CDP 客户端会连接到该字段中的 URL。curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused 表示没有进程正在监听,因此应查看启动服务器的终端;如果服务器已作为服务运行,则使用 journalctl -u moli -n 50/json/list 列出已打开的目标,/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)都没有可作用的进程。代理、Cookie 和 user-agent 设置也应作为 Moli 服务器自身的参数传入,原因相同。应将其视为部分 CDP 支持,而不是完整的 Chrome 协议:明确返回的“不支持方法”错误表示引擎的边界,不是代码错误。

两个服务器参数决定 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(比例集大小,即在共享页面的进程之间拆分计算共享页面内存后的内存占用)为 102.46 MiB,而 Chromium 为 348.82 MiB。结构上的差异体现在最后一列:Moli 使用 1 个进程,而 Chromium 使用 11 个进程。一个进程意味着 systemd 只需监督一个对象,一个 cgroup 只需设置一项上限,因此下一节可以保持简短。

在 loopback 上将 moli serve 作为 systemd 服务运行

当 agent 需要一个等待连接的浏览器时,将服务器作为服务运行。如果 agent 不需要浏览器,请继续按每个 URL 使用 moli fetch,因为空闲服务器仍会占用内存。

不要将端口 9222 绑定到公共接口。CDP 完全没有身份验证步骤。任何可以访问该端口的人都能控制浏览器,并读取浏览器可以访问的所有内容,包括 profile 目录中的任何 cookie。将其绑定到 127.0.0.1。从另一台机器访问时,请通过 SSH 隧道(ssh -L 9222:127.0.0.1:9222 user@your-vps)或私有 VPN 接口连接,并让 agent 在隧道的另一端连接到 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 超过限制时,内核会终止其中的某个进程,而不会影响服务器上的其他进程。journal 会记录相关信息:

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

将该行作为容量规划信号。如果页面的资源开销高于预期,或者限制设置得过低,就需要调整。请根据自有页面的测量结果设置该数值,下一节将介绍测量方法。同样的 accounting 标志也可以限制服务器上的其他服务,使用 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 会对峰值做出反应。

对于服务,请读取内核已经为其 cgroup 保存的计数器:

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

memory.peak 是字节数,也是该单元自上次启动以来的历史最高值,因此重启后会重置。MemoryMax 的值必须高于该数值,并为尚未访问的最复杂页面预留额外空间。systemd-cgtop -m 按单元显示实时使用量,这是查看服务器上当前哪个服务开销最高的最快方法。

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 大约有五分之一的任务失败,而 Chrome 可以完成这些任务。如果你的代理只访问你控制的一组固定页面,这个比例几乎没有参考价值,因为页面要么能正常工作,要么不能,你可以在今天内查明原因。如果你的代理浏览开放网络,这就是必须在设计中考虑的实际失败率。

根据项目声明的范围,可以预见以下情况会失败:

  • 将界面绘制到 Canvas 元素而不是 DOM 中的应用,因为 Canvas 保真度明确不在支持范围内
  • 需要 WebGL 或 GPU 合成的内容,因为没有 GPU 合成器
  • 受 DRM 保护的视频和要求较高的媒体播放
  • 对照 Chrome 验证像素级精确屏幕截图的视觉测试,因为与 Chrome 保持一致并不是项目目标

项目引用的另一个数据是,一次完整运行通过了 1.612 million 个 Web Platform Tests。这个数据说明的是标准覆盖范围,并不代表你的代理要访问的网站都能正常工作。页面即使只使用支持良好的标准,也可能在机器人检查环节失败;任何引擎得分都无法覆盖这种情况。

因此,应在设计中保留回退方案。先将每个 URL 发送给 Moli。页面返回空内容或某个选择器始终不出现时,再让 Playwright 驱动真实的 Chrome,重试该 URL。可以在更大的机器上运行,也可以安排在能够承担 773 MiB 进程的时间运行。大多数代理的大部分时间都在处理普通页面,因此由轻量引擎承担主要请求量,由高成本引擎处理少数异常情况。

FAQ

Moli 能替代代理使用的无头 Chrome 吗?

如果只是读取页面、提取文本和执行普通点击,通常可以。在项目自己的基准测试中,Moli 完成了 1,308 个任务中的 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。相比之下,无头 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 隧道或私有 VPN 接口从另一台机器访问该端点。如果必须绑定到其他地址,请将其绑定到私有接口,并使用防火墙控制访问。

为什么我的屏幕截图为空,或者点击没有落在任何位置?

默认情况下,布局功能处于禁用状态。README 将默认策略设为 LayoutPolicy::Mock,因此元素几何信息不是真实值,任何依赖页面元素框的功能都无法工作。使用 moli serve --layout 启动服务器,或将 --layout 添加到 moli fetch,屏幕截图和坐标操作就会开始工作。缺少图像由另一个标志控制:--resource

我应该安装哪个版本的 Moli?

固定一个版本,并记录所用版本。截至 August 2026,当前发行版为 1.0.1;而项目发布的基准测试数据是在 0.1.1 上测得的。因此,与他人比较结果时,不能将这两个版本视为可互换。下载该标签对应的 moli-x86_64-unknown-linux-gnu.tar.gz,然后自行安装二进制文件,不要依赖 shell 安装程序。shell 安装程序会解析最新发行版,而不是解析你获取的标签对应版本。最后使用 moli --version 确认安装结果。