MAATRIX / Блог / Firecrawl на сервере: частые ошибки и решения

Firecrawl на сервере: частые ошибки и решения

Firecrawl на сервере: частые ошибки и решения

MAATRIX

docker compose up -d --build отработал, docker compose ps показывает семь контейнеров вместо ожидаемых двух-трёх, а первый же запрос к Firecrawl либо висит, либо возвращает pageStatusCode: 403 там, где ждали текст страницы. Стек сложнее, чем кажется по README: API с воркерами, Chromium в Playwright, Redis, RabbitMQ и очередь на Postgres — у каждого слоя свой набор ошибок. Разберём их по порядку, с точными текстами из исходников, а не советом «перезапустите контейнер».

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Карта стека: семь контейнеров Firecrawl и что каждый обязан уметь

Self-host Firecrawl — это не «один процесс», а docker-compose.yaml на семь сервисов. Раньше хватало Redis и Supabase; сейчас добавилась очередь на RabbitMQ и Postgres.

СервисРольПортНаружу
apiREST API + воркеры (harness.js поднимает ещё extract-worker на 3004 и queue-worker на 3005)3002да, ${PORT:-3002}
playwright-serviceheadless Chromium и собственный SSRF-прокси3000нет
redisкэш очереди и рейт-лимитов6379нет
rabbitmqброкер очереди NuQ5672нет
nuq-postgresPostgres 17 + pg_cron, хранит саму очередь5432нет
foundationdb / foundationdb-initопциональный бэкенд очереди (NUQ_BACKEND=fdb)4500нет

Наружу по умолчанию торчит только api — в SELF_HOST.md это прописано прямо: «Only the API is published to the host by default». Первая проверка после старта — не чтение логов вслепую, а обвязка:

docker compose ps
curl -s http://127.0.0.1:3002/

Живой api отвечает буквально {"message":"Firecrawl API","documentation_url":"https://docs.firecrawl.dev"}. Тишина или Connection refused — до Playwright и очередей дело ещё не дошло, разбирайтесь с оркестрацией контейнеров (следующий раздел).

До playwright-service так просто не достучаться: порта 3000 на хосте нет, а в образе node:22-slim, в отличие от полного node:22, нет ни curl, ни wget. Рабочий способ — дёрнуть сервис изнутри контейнера api штатным fetch из Node 22:

docker compose exec api node -e "fetch('http://playwright-service:3000/health').then(r=>r.text()).then(console.log)"

Здоровый сервис вернёт {"status":"healthy","maxConcurrentPages":10,"activePages":0}.

`docker compose up`: частые ошибки запуска и зависшие контейнеры

Первая проблема обычно не про Firecrawl, а про порядок запуска. У api три зависимости, и они не равноценны:

depends_on:
  redis:
    condition: service_started
  playwright-service:
    condition: service_started
  rabbitmq:
    condition: service_healthy

От redis и playwright-service требуется только «процесс стартовал», а rabbitmq обязан пройти healthcheck:

healthcheck:
  test: ["CMD", "rabbitmq-diagnostics", "-q", "check_running"]
  interval: 5s
  timeout: 5s
  retries: 3
  start_period: 5s

На слабом vCPU Erlang-машина RabbitMQ поднимается дольше отведённых трёх попыток по пять секунд — и контейнер api даже не создаётся: docker compose ps его не покажет, а docker compose up зависнет в ожидании rabbitmq. Лечится без магии: docker compose up -d rabbitmq, дождаться healthy в выводе docker compose ps, затем docker compose up -d — Compose досоздаст остальное.

Обратите внимание: nuq-postgres в списке зависимостей api вообще нет. На первом холодном старте, пока Postgres выполняет инициализационный nuq.sql, api уже пытается к нему стучаться — от этой гонки Compose не защищает. Если первый запуск падает с ошибками подключения к базе, а docker compose restart api следом проходит гладко — дело в этом, а не в конфиге.

Второй источник затыков — сам api стартует командой node dist/src/harness.js --start-docker, которая поднимает внутри контейнера не один процесс, а минимум три: API, extract-worker на 3004 и queue-worker на 3005. На это отведён HARNESS_STARTUP_TIMEOUT_MS — по умолчанию 60000 мс. На перегруженном хосте под-процессы не успевают подняться, api уходит в перезапуск по кругу между Up и Restarting. Поднимите таймаут до 120000 и проверьте память хоста — более частая причина, разбор ниже.

Третье — путаница с портом. В маппинге "${PORT:-3002}:${INTERNAL_PORT:-3002}" переменная PORT отвечает только за то, что слушает хост; порт, на который слушает процесс внутри контейнера, задаёт отдельная INTERNAL_PORT (тоже по умолчанию 3002). Прописать PORT=8080 в .env — нормальный способ вынести Firecrawl на другой хостовый порт, внутри контейнера ничего не ломается. А вот менять INTERNAL_PORT без нужды не стоит — рассинхронизируете две половины одного маппинга.

И последнее, что удивляет новичка: в файле нет profiles:, поэтому docker compose up -d --build поднимает foundationdb и foundationdb-init всегда — даже если NUQ_BACKEND=fdb вы ни разу не выставляли. foundationdb-init — одноразовый контейнер, выполняет configure new single ssd и корректно завершается со статусом Exited (0), это не ошибка. А сам foundationdb остаётся висеть и есть память впустую, если очередь и так работает через nuq-postgres. Не используете fdb — остановите обе: docker compose stop foundationdb foundationdb-init.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

`USE_DB_AUTHENTICATION`: ловушка между `.env.example` и `docker-compose.yaml`

Здесь два официальных файла проекта прямо расходятся, и об этом стоит знать заранее. Сам docker-compose.yaml берёт по умолчанию false:

USE_DB_AUTHENTICATION: ${USE_DB_AUTHENTICATION:-false}

А в apps/api/.env.example — том самом, который интуитивно хочется скопировать в .env, — обратная настройка:

# To turn on DB authentication, you need to set up supabase.
USE_DB_AUTHENTICATION=true

SELF_HOST.md не оставляет простора для догадок: «API authentication: USE_DB_AUTHENTICATION=false» для первого запуска, и отдельной строкой — «Do not use apps/api/.env.example as a drop-in Compose contract». То есть apps/api/.env.example — справочник переменных приложения, а не заготовка для корневого .env.

Если всё же сделать cp apps/api/.env.example .env, получите USE_DB_AUTHENTICATION=true при пустых SUPABASE_URL, SUPABASE_ANON_TOKEN и SUPABASE_SERVICE_TOKEN — авторизацию включили, а проверять её нечем. Это хуже честного «выключено»: выглядит как защищённый API, а по факту не работает предсказуемо.

Обратная сторона тоже честная: при USE_DB_AUTHENTICATION=false (рекомендованном значении) реальной проверки Bearer-токена нет вовсе — любой, кто достучится до 3002, может дёргать /v1/scrape без ключа, ограниченный только keyless-лимитом по IP. Для локальных тестов это осознанный дефолт из документации. Для сервера в интернете — уже нет: держите 3002 за Nginx с TLS, ufw deny 3002/tcp снаружи, доступ только с адреса прокси.

RabbitMQ, Postgres и переменные, которые `.env` не меняет

Не каждая переменная в .env реально куда-то долетает. Адрес очереди у api прописан прямо в docker-compose.yaml, а не унаследован из общего блока x-common-env:

NUQ_RABBITMQ_URL: amqp://rabbitmq:5672

Решите вынести RabbitMQ на отдельный сервер или взять управляемый — записать NUQ_RABBITMQ_URL=amqp://... в .env бесполезно, эта переменная туда не пробрасывается. Менять нужно сам docker-compose.yaml.

Второй сюрприз — на стороне Postgres. Образ nuq-postgres собирается из postgres:17 с добавленным pg_cron, и в его Dockerfile целевая база для cron зашита намертво:

cron.database_name = 'postgres'

Если по привычке сменить POSTGRES_DB=firecrawl в .env — база создастся под этим именем, но pg_cron продолжит ждать задания в базе, которая буквально называется postgres. Не трогайте POSTGRES_DB, если не готовы пересобирать образ nuq-postgres с другим cron.database_name.

Учётные данные Postgres по умолчанию тоже говорящие: POSTGRES_USER, POSTGRES_PASSWORD и POSTGRES_DB совпадают и равны postgres. В стоковом файле не страшно — 5432 у nuq-postgres не публикуется, база доступна только из backend-сети Docker. Добавите ports: ["5432:5432"] для отладки — сразу смените пароль, иначе первое же сканирование снаружи найдёт голый Postgres с дефолтным паролем.

Playwright и SSRF-защита: почему `curl` видит 200, а Firecrawl — 403

Самая непонятная на первый взгляд ошибка приходит не от Chromium, а от собственной защиты Firecrawl. playwright-service прогоняет весь трафик через внутренний SSRF-прокси и до навигации резолвит хост по DNS: если адрес попадает в приватный или локальный диапазон, запрос блокируется классом InsecureConnectionError с сообщением вида:

Blocked insecure target URL "http://192.168.1.10:8080/": resolves to a private/internal address

Ловушка в том, как эта ошибка доезжает до вызывающей стороны: HTTP-статус самого ответа — 200, а не 403. Внутри JSON лежит pageStatusCode: 403 и pageError с текстом выше, content пустой. Если проверяете успех скрейпа по curl -o /dev/null -w "%{http_code}", увидите обманчивую 200 там, где реального контента нет, — смотреть нужно в тело ответа, а не в код ответа.

Это осознанное поведение, не баг: так Firecrawl не даст себя использовать, чтобы прощупать вашу собственную внутреннюю сеть или дёрнуть адрес метаданных облака через параметр URL. Если действительно нужно скрейпить внутренний адрес — свой стейджинг на той же машине, локальный тестовый сайт, — разрешите это явно:

ALLOW_LOCAL_WEBHOOKS=TRUE

в .env, с перезапуском playwright-service.

Отдельно от SSRF стоит проверка прокси. Без PROXY_SERVER в переменных Firecrawl честно предупреждает об этом в логах при каждом запросе:

⚠️ WARNING: No proxy server provided. Your IP address may be blocked.

Это не ошибка выполнения — скрейп пойдёт, но с чистого IP сервера, и на сайтах с защитой от ботов доля отказов будет заметно выше. Переменные PROXY_SERVER/PROXY_USERNAME/PROXY_PASSWORD пробрасываются ровно туда, куда нужно, — в контекст браузера playwright-service; сама настройка прокси для парсинга — тема отдельного разбора, как установить прокси для парсинга на VPS, а частые проблемы с самими прокси — капча, отвал соединений, утечка реального IP — разобраны в статье про частые ошибки прокси для парсинга.

Дальше — обычные ошибки конкретной страницы. Если соединение вообще не дошло до ответа — DNS не резолвится, порт закрыт, таймаут истёк раньше первого байта, — pageStatusCode будет null, а pageError — буквально No response received. Указали check_selector, а он не появился на странице за отведённый timeout (по умолчанию 15000 мс) — получите Required selector not found; для тяжёлых SPA увеличивайте оба параметра в запросе, а не гадайте с ретраями. Любая необработанная ошибка внутри самого скрейпа отдаётся наружу одинаково сухо — 500 и {"error":"An error occurred while fetching the page."}; реальная причина остаётся только в логах сервиса:

docker compose logs -f --tail 100 playwright-service

Что приятно — классический для Docker краш headless-Chromium из-за тесного /dev/shm тут закрыт заранее: браузер в playwright-service запускается с флагом --disable-dev-shm-usage из коробки, вместе с --no-sandbox и --disable-gpu. Отдельно увеличивать --shm-size на уровне Docker не требуется — если Chromium всё же падает, ищите причину в лимите памяти контейнера (mem_limit: 4G) и в MAX_CONCURRENT_PAGES, а не в размере /dev/shm.

Сколько памяти реально просит стек: считаем по лимитам, а не наугад

Замеров производительности Firecrawl мы не публикуем — токены тут ни при чём, а скорость скрейпа зависит от целевого сайта сильнее, чем от вашего железа. Зато у самого проекта в docker-compose.yaml уже есть честные цифры — не бенчмарк, а потолки, которые Compose ставит контейнерам:

# api
cpus: 4.0
mem_limit: 8G
# playwright-service
cpus: 2.0
mem_limit: 4G

Это mem_limit — разрешённый максимум, а не гарантированное потребление с первой секунды, но масштаб он задаёт недвусмысленно: только эти два контейнера из семи авторы готовы допустить до 12 ГБ суммарно. Добавьте Redis, RabbitMQ и Postgres рядом — и на VPS с 2 ГБ RAM всей семёрке банально негде развернуться: api уйдёт в OOM почти сразу, как только под нагрузкой откроется вторая-третья вкладка Chromium.

Рычаги, которыми реально можно ужать потребление под небольшой сервер, — конкретные переменные .env, у всех дефолты с расчётом на боевую нагрузку:

ПеременнаяПо умолчаниюЧто регулирует
NUM_WORKERS_PER_QUEUE8воркеры очереди внутри harness.js
CRAWL_CONCURRENT_REQUESTS10параллельные страницы (и MAX_CONCURRENT_PAGES в Playwright)
MAX_CONCURRENT_JOBS5одновременные задания crawl
BROWSER_POOL_SIZE5пул экземпляров браузера

На небольшом сервере честнее не бороться с OOM свопом, а срезать все четыре разом до 23: меньше параллельных вкладок Chromium — меньше пиковая память, пусть и ценой более длинной очереди на объёмных crawl-заданиях. Подробный расчёт под конкретный профиль нагрузки — в материале сколько ресурсов нужно VPS для парсинга и скрейпинга; он актуален и для безголового браузера Firecrawl, не только для HTTP-парсеров.

Ориентир, который мы даём как расчёт от документированных лимитов, а не как измеренный результат: для тестового контура с урезанной параллельностью хватает 4 vCPU / 8 ГБ RAM / 60 ГБ NVMe; для боевого использования со стоковыми значениями концурентности — 8 vCPU / 16 ГБ RAM / 150 ГБ NVMe, с запасом под кэш Chromium, растущую базу Postgres и логи.

Какой сервер под Firecrawl брать в MAATRIX

Firecrawl в каталоге готовых приложений MAATRIX пока нет — сервер под него приходит чистым (Ubuntu 24.04 или Debian 12), а стек вы поднимаете docker compose up -d --build по конфигурации из этой статьи. Это честно медленнее, чем LiteLLM или n8n из каталога с готовыми доступами сразу после оплаты, — зато полный контроль над тем, какие из семи сервисов вообще нужны.

Раз минимальный работающий контур — это api + playwright-service + redis + rabbitmq + nuq-postgres, берите сервер, отталкиваясь от лимитов из предыдущего раздела, а не от привычки «1 vCPU и 1 ГБ хватит на любой self-host». Реалистичный минимум — 4 vCPU / 8 ГБ RAM / 60 ГБ NVMe с урезанной конкурентностью; комфортный вариант под боевой crawl — 8 vCPU / 16 ГБ RAM / 150 ГБ NVMe без правки дефолтов.

Локация — Великобритания, Лондон. У Firecrawl задача — ходить на чужие сайты по всему миру, и здесь важны репутация исходящего IP и задержка до целей. Антибот-системы вроде Cloudflare жёстче реагируют на подсети из российских диапазонов независимо от легитимности краулера — чистый европейский IP снижает долю блокировок ещё до подключения PROXY_SERVER. По задержке Лондон ощутимо ближе к основной массе европейских и англоязычных сайтов, чем Россия, — на crawl-задании из полусотни страниц это складывается в заметную разницу по времени прогона.

Если цель — не разовый скрейп, а пайплайн вокруг него, часть соседних сервисов в каталоге MAATRIX уже есть и разворачивается автоматически: n8n — забирать результат /v1/scrape вебхуком (разбор — в статье как поднять n8n для AI-автоматизаций на сервере), и векторная база Qdrant — если текст должен лечь в RAG, а не просто сохраниться файлом. Firecrawl здесь — источник данных, а не всё решение сразу.

Оплата — картами российских банков, по СБП, криптовалютой или токеном MAAT; для сервера в Лондоне зарубежная карта не нужна.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Частые вопросы

Почему docker compose ps показывает семь контейнеров, если я не просил FoundationDB?

В файле нет profiles:, поэтому docker compose up поднимает всё описанное, включая foundationdb и одноразовый foundationdb-init — резервный бэкенд очереди на случай NUQ_BACKEND=fdb, а не обязательный компонент. На очереди по умолчанию (nuq-postgres) оба можно остановить: docker compose stop foundationdb foundationdb-init.

Firecrawl без Supabase — рабочая конфигурация или временный костыль?

Рабочая и официально рекомендованная для старта: SELF_HOST.md прямо советует USE_DB_AUTHENTICATION=false для первого запуска. Supabase нужен только для авторизации по отдельным ключам и части логирования; без него /v1/scrape, /v1/crawl и /v1/map работают полностью, просто без деления доступа между командой.

Почему Firecrawl отказывается парсить адрес на моём же сервере — localhost или внутренний IP?

Встроенная SSRF-защита playwright-service резолвит хост по DNS и блокирует приватные и локальные диапазоны ещё до перехода на страницу, отдавая pageStatusCode: 403 при формальном HTTP 200. Если это осознанная задача — свой стейджинг, внутренний тестовый сайт, — разрешите явно переменной ALLOW_LOCAL_WEBHOOKS=TRUE в .env и перезапустите сервис.

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.