Firecrawl на сервере: частые ошибки и решения
docker compose up -d --build отработал, docker compose ps показывает семь контейнеров вместо ожидаемых двух-трёх, а первый же запрос к Firecrawl либо висит, либо возвращает pageStatusCode: 403 там, где ждали текст страницы. Стек сложнее, чем кажется по README: API с воркерами, Chromium в Playwright, Redis, RabbitMQ и очередь на Postgres — у каждого слоя свой набор ошибок. Разберём их по порядку, с точными текстами из исходников, а не советом «перезапустите контейнер».
Содержание
- Карта стека: семь контейнеров Firecrawl и что каждый обязан уметь
- `docker compose up`: частые ошибки запуска и зависшие контейнеры
- `USE_DB_AUTHENTICATION`: ловушка между `.env.example` и `docker-compose.yaml`
- RabbitMQ, Postgres и переменные, которые `.env` не меняет
- Playwright и SSRF-защита: почему `curl` видит 200, а Firecrawl — 403
- Сколько памяти реально просит стек: считаем по лимитам, а не наугад
- Какой сервер под Firecrawl брать в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Карта стека: семь контейнеров Firecrawl и что каждый обязан уметь
Self-host Firecrawl — это не «один процесс», а docker-compose.yaml на семь сервисов. Раньше хватало Redis и Supabase; сейчас добавилась очередь на RabbitMQ и Postgres.
| Сервис | Роль | Порт | Наружу |
|---|---|---|---|
api | REST API + воркеры (harness.js поднимает ещё extract-worker на 3004 и queue-worker на 3005) | 3002 | да, ${PORT:-3002} |
playwright-service | headless Chromium и собственный SSRF-прокси | 3000 | нет |
redis | кэш очереди и рейт-лимитов | 6379 | нет |
rabbitmq | брокер очереди NuQ | 5672 | нет |
nuq-postgres | Postgres 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_QUEUE | 8 | воркеры очереди внутри harness.js |
CRAWL_CONCURRENT_REQUESTS | 10 | параллельные страницы (и MAX_CONCURRENT_PAGES в Playwright) |
MAX_CONCURRENT_JOBS | 5 | одновременные задания crawl |
BROWSER_POOL_SIZE | 5 | пул экземпляров браузера |
На небольшом сервере честнее не бороться с OOM свопом, а срезать все четыре разом до 2–3: меньше параллельных вкладок 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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.