Как запустить Moli для AI-агента на VPS
Узнайте, как установить Moli, открыть CDP на loopback и подключить Playwright. Разберите ограничения Canvas, GPU и мультимедиа, из-за которых нужен Chrome.
Headless-браузер для небольшого VPS
Moli — это headless-браузер для AI-агентов. Он достаточно компактен для самостоятельного запуска на VPS, где headless Chrome не поместится. Это браузерный движок, написанный на Rust, а не оболочка вокруг Chromium. Он поддерживает Chrome DevTools Protocol (CDP) — протокол, с которым уже работает ваша библиотека автоматизации. Установите один бинарный файл, запустите moli serve, а затем укажите в Playwright или собственном коде агента адрес http://127.0.0.1:9222.
Перед установкой ознакомьтесь с ограничениями. Проект прямо указывает область применения: графического браузера нет, GPU-композитора нет, попиксельного соответствия Chrome нет, а поддержка Canvas и воспроизведения мультимедиа не обеспечивает высокой точности. Страницы, которым нужны эти возможности, не будут работать. Real Chrome под управлением Playwright остаётся запасным вариантом. В последнем разделе показано, как определить, каким страницам он требуется.
Все приведённые ниже команды взяты из README проекта и опубликованных им skill-файлов, проверенных в августе 2026 года. Все числа на диаграммах — значения, опубликованные проектом для собственного движка, а не результаты измерений на этом сайте. Это указано в подписи к каждой диаграмме. Если вы ещё выбираете движок, в обзорном материале headless-браузеры для агентов на VPS рассмотрены альтернативы.
Почему headless Chrome потребляет так много памяти?
Chrome — многопроцессный браузер. Каждая вкладка и каждый iframe с другим сайтом получают собственный процесс renderer, а каждый renderer использует собственную кучу V8 и собственные графические буферы. Такая архитектура подходит для настольного компьютера: сбой одной вкладки не должен завершать работу всего окна. На VPS с 2 GB это означает, что один шаг просмотра может потребовать больше памяти, чем само запущенное приложение.
Проект обработал 192 смешанных общедоступных URL с помощью четырёх движков и опубликовал результат.
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, поэтому в этой выборке оба движка обработали примерно одинаковую долю веб-сайтов. Различие проявляется в потреблении памяти: медианное значение RSS (resident set size, объём памяти, который процесс фактически удерживает в RAM) составило 773 MiB для Chrome и 73 MiB для Moli. Такое направление сравнения выглядит правдоподобно, поскольку оно следует из архитектуры процессов. Точное соотношение для ваших страниц не следует считать заранее заданным.
Медианное значение не определяет проблему. Проблему определяет пиковое значение. Когда на сервере с 2 GB заканчивается память, kernel выбирает процесс и завершает его работу. Запись об этом появляется в dmesg -T или 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Ваш агент эту строку не видит. Он видит браузер, который перестал отвечать. Обычно это проявляется как ошибка Playwright, например page.goto: Page crashed, или как закрытый target. В этой ошибке не упоминается память. Поэтому при случайных сбоях агента на небольшом сервере первым нужно проверить OOM (out of memory) killer. Расчёт ресурсов с учётом пикового потребления выполняется так же, как выбор RAM и CPU для VPS агента.
Установите бинарный файл Moli с фиксацией версии
Проект публикует shell-инсталлятор и готовые архивы tarball в релизах GitHub. По состоянию на August 2026 текущий релиз — 1.0.1, опубликованный 18 August 2026. Приведённые в этом руководстве результаты benchmark были измерены проектом на версии 0.1.1. Поэтому рассматривайте их как примерное представление о работе движка, а не как гарантию для устанавливаемой сборки.
Зафиксируйте версию. Инсталлятор, который всегда разрешает latest, при следующей пересборке переведёт ваш агент на другой browser engine. Изменение поведения браузера нужно планировать, а не обнаруживать случайно.
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, поэтому поддерживаются VPS-планы и на Arm, и на x86. Укажите MOLI_INSTALL_DIR, чтобы установить файл в другое место. Обратите внимание, какую версию выбирает скрипт: самый новый релиз, а не тег, из которого вы получили скрипт. Для первого ознакомления это допустимо. Для воспроизводимой пересборки такой подход не подходит.
Поэтому для постоянной установки вручную выполните те же действия, что и инсталлятор, и самостоятельно укажите точный архив. Так вы также разместите бинарный файл там, где его сможет использовать системная служба, и не будете передавать загруженный скрипт в shell через 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, выводящий зафиксированную вами версию, — это вся необходимая проверка. 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 ожидает указанный вами селектор. Это единственная стратегия, которая учитывает содержимое загружаемой страницы. Поэтому она наиболее надёжна, если целевой элемент известен.
Для screenshots и 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, найденных агентом, а не выбранных вами, добавьте --block-private-networks. Агент, который переходит по ссылкам, найденным на странице, может случайно обратиться к http://169.254.169.254/ с учётными данными облачного инстанса или к порту базы данных на localhost, который не предназначен для доступа из Интернета. Этот флаг запрещает переходы в частное адресное пространство, а --block-cidrs дополнительно ограничивает такие переходы. Если задача состоит в обходе страниц, а не в чтении одной страницы, структура такого конвейера описана в материале самостоятельно размещаемые альтернативы Firecrawl. Предыдущий этап, то есть поиск самих URL, описан в материале навык поиска для агентов на основе SearXNG.
Подключите agent к Moli через CDP
Для agent, который выполняет множество переходов и нажатий, вместо этого запустите server.
moli serve --host 127.0.0.1 --port 9222127.0.0.1 и порт 9222 используются по умолчанию, поэтому обычная команда moli serve привязывает server только к loopback. Тем не менее в постоянной конфигурации указывайте оба параметра явно. Следующему администратору не придётся вспоминать, каким было значение по умолчанию.
Перед подключением client проверьте server:
curl -s http://127.0.0.1:9222/json/versionРабочий server отвечает JSON-объектом с полем webSocketDebuggerUrl. Это URL, к которому подключается CDP client. Ошибка curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused означает, что ни один процесс не прослушивает порт. В этом случае проверьте терминал, из которого запущен server, либо выполните journalctl -u moli -n 50, если server работает как service. Команда /json/list выводит открытые targets, а /json/protocol — domains, реализованные в этой сборке. Так можно проверить, поддерживается ли нужный вам CDP method.
Playwright подключается к этой endpoint вместо собственного запуска 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();Важна строка connectOverCDP, а не chromium.launch(). Здесь нет дочернего процесса Chromium, поэтому executablePath и стандартные container flags, например --no-sandbox, не применяются. Настройки proxy, cookie и user-agent передаются server Moli как его собственные flags по той же причине. Поддерживается только часть CDP, а не весь Chrome protocol. Явная ошибка о неподдерживаемом method обозначает ограничение engine, а не ошибку в вашем коде.
Два server flags определяют возможности agent. --layout включает реальные геометрические данные. Они нужны для нажатий по координатам и создания screenshots. --resource загружает дополнительные images, fonts и media. Это увеличивает расход bandwidth и memory при каждой загрузке страницы, поэтому оставляйте flag выключенным, пока страница явно не потребует его. --profile-dir сохраняет cookies и storage между запусками. Без этого каждый запуск начинается с чистого состояния.
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
}
]В workload sample agent из проекта Moli установил CDP connection за 34.85 ms против 169.37 ms у Chromium. Пиковый PSS (proportional set size, объём памяти с распределением shared pages между процессами, которые их используют) составил 102.46 MiB против 348.82 MiB. Структурное различие видно в последнем столбце: 1 process против 11. Один process означает один объект для systemd и один cgroup для ограничения ресурсов. Поэтому следующий раздел будет коротким.
Запуск moli serve как службы systemd на loopback
Запускайте сервер как службу, если агенту нужен постоянно доступный браузер. Если браузер не нужен, по-прежнему используйте moli fetch для каждого URL, поскольку простаивающий сервер всё равно занимает память.
Не размещайте порт 9222 на публичном интерфейсе. CDP не поддерживает аутентификацию. Любой, кто может подключиться к этому порту, сможет управлять браузером и читать всё, к чему браузер имеет доступ, включая cookies в каталоге вашего профиля. Оставьте его на 127.0.0.1. Подключайтесь к нему с другой машины через SSH-туннель (ssh -L 9222:127.0.0.1:9222 user@your-vps) или через интерфейс частной VPN, а агент пусть подключается к 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.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 должен вывести active (running), а curl должен вернуть JSON для обнаружения. ProtectSystem=strict монтирует всю файловую систему только для чтения для этого unit. Поэтому StateDirectory=moli здесь обязателен: он создаёт /var/lib/moli, назначает его владельцем пользователя службы и делает доступным для записи только этот путь. Если unit запускается, а затем завершается с ошибкой доступа в journalctl -u moli, почти всегда процесс пытается записать в каталог, который ProtectSystem только что сделал доступным только для чтения. Переместите этот путь в каталог состояния.
MemoryMax=768M обеспечивает безопасный запуск рядом с вашим приложением. Unit получает собственную cgroup. Когда cgroup превышает установленный лимит, ядро завершает один из процессов внутри неё и не затрагивает остальную систему. В журнале появляется запись:
moli.service: A process of this unit has been killed by the OOM killer.Используйте эту строку как ориентир для настройки размера лимита. Возможно, страницы тяжелее, чем вы предполагали, либо лимит слишком низкий. Задайте значение по результатам измерения собственных страниц. Это рассматривается в следующем разделе. Те же параметры учёта ограничивают любой другой сервис в системе, а ограничение памяти и CPU с помощью systemd действует для остальных сервисов.
Измерьте пиковое потребление памяти самостоятельно
Опубликованные значения получены на чужом оборудовании и при загрузке чужих страниц. От пикового потребления памяти зависит, переживёт ли ваш сервер нагрузку. Пиковое значение полностью зависит от того, что именно вы загружаете. Измерьте потребление до выбора ресурсов.
Для разового получения страниц используйте бинарный файл time. Он выводит значительно больше данных, чем встроенная команда оболочки с тем же именем:
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. Запустите команду для десяти страниц, которые ваш агент действительно посещает, а не для example.com. Сохраняйте максимальный результат, а не среднее значение, поскольку OOM killer реагирует на пики.
Для сервиса прочитайте счётчик, который kernel уже ведёт для его cgroup:
cat /sys/fs/cgroup/system.slice/moli.service/memory.peak
systemd-cgtop -mmemory.peak — это количество байт и максимальное значение с момента последнего запуска unit, поэтому перезапуск сбрасывает его. Именно это значение должно быть ниже MemoryMax, причём необходимо оставить запас для самой тяжёлой страницы, которую вы ещё не посещали. systemd-cgtop -m показывает текущее потребление по каждому unit. Это самый быстрый способ определить, какой сервис на сервере сегодня потребляет больше всего ресурсов.
Где Moli не справляется и когда всё ещё нужен Chrome?
Проект также проводит тестирование 1,308 сопоставимых задач автоматизации браузера и публикует результаты для нескольких движков.
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 процентов. Проект оценивает себя на собственном наборе тестов, поэтому этот показатель следует считать заявлением проекта, а не независимым результатом.
Практический вывод прост. Примерно одна из пяти задач, выполненных Chrome, не была выполнена Moli. Если ваш агент посещает фиксированный набор страниц, которыми вы управляете, это соотношение мало о чём говорит: ваши страницы либо работают, либо нет, и это можно проверить в тот же день. Если агент работает с открытым вебом, это реальная частота сбоев, которую нужно учитывать при проектировании.
Причины сбоев предсказуемы по заявленной области применения проекта.
- Приложения, которые отрисовывают интерфейс в элементе Canvas, а не в DOM, поскольку поддержка Canvas явно не входит в область проекта
- Всё, что требует WebGL или GPU-композиции, поскольку GPU-композитор отсутствует
- Видео с защитой DRM и ресурсоёмкое воспроизведение мультимедиа
- Визуальные тесты, сравнивающие скриншоты с Chrome на уровне отдельных пикселей, поскольку соответствие Chrome не является целью проекта
Другой показатель, который приводит проект, — прохождение 1.612 миллиона тестов веб-платформы за один полный запуск — характеризует поддержку стандартов. Он не означает, что сайты, которые посетит ваш агент, будут работать. Страница может использовать только хорошо поддерживаемые стандарты и всё равно не пройти проверку для ботов. Ни один показатель движка этого не отражает.
Поэтому предусмотрите fallback в архитектуре. Сначала передавайте каждый URL в Moli. Если страница возвращается пустой или нужный селектор не появляется, повторно обработайте этот URL через Playwright, управляющий настоящим Chrome, на более мощной машине или по расписанию, при котором процесс объёмом 773 MiB допустим. Большую часть времени агенты работают с обычными страницами, поэтому небольшой движок обрабатывает основной объём, а более ресурсоёмкий — редкие сложные случаи.
FAQ
Может ли Moli заменить headless Chrome для моего агента?
Для чтения страниц, извлечения текста и обычных щелчков обычно да. В собственном бенчмарке проекта на 1,308 задачах Moli выполнил 81.88 процентов против 99.85 процентов у Chrome, поэтому примерно для одной задачи из пяти требуется то, чего Moli не умеет. Известные ограничения — приложения, отрисовываемые через Canvas, WebGL и видео с DRM. Направляйте такие URL в обычный Chrome, а не переключайте обратно всё окружение.
Сколько RAM требуется Moli на VPS?
Проект сообщает о медианном RSS в 73 MiB при обходе 192 URL и пиковом PSS в 102.46 MiB в одном тестовом эпизоде агента. Для headless Chrome медианный показатель составляет 773 MiB. Это значения, измеренные проектом на его страницах. Для однократного запуска измерьте свой показатель с помощью /usr/bin/time -v вокруг вызова moli fetch. Для сервиса прочитайте /sys/fs/cgroup/system.slice/moli.service/memory.peak. Затем задайте значение MemoryMax выше максимального показателя, который вы получили.
Безопасно ли открывать порт 9222 в Интернет?
Нет. В CDP нет аутентификации. Поэтому любой, кто может подключиться к этому порту, получит управление браузером и сможет читать всё, что доступно браузеру. Оставьте --host 127.0.0.1 и подключайтесь к endpoint с другой машины через SSH-туннель или частный интерфейс VPN. Если необходимо привязать другой адрес, используйте частный интерфейс и ограничьте доступ с помощью firewall.
Почему мой скриншот пуст или щелчок выполняется не по элементу?
По умолчанию layout отключён. В 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 installer. Он выбирает самый новый релиз, а не тег, из которого вы скачали файл. Затем проверьте версию с помощью moli --version.