Daily Archives: 2026-09-17


A local Firecrawl for my Hermes agent

My Hermes Agent does a lot of web work: it extracts the content of pages as clean markdown, and it searches the web. Both go through Firecrawl, a scraping API that exists as a hosted cloud service and as an open source, self-hostable stack. When I described my Hermes setup I mentioned the Firecrawl integration, but not where it points: to my own instance of Firecrawl, running in Docker next to the agent on my home server.

Why self-host it

  • No usage meter. The cloud plan is usage-based, and an agent is bursty: several pages per question, many questions per day.
  • Privacy. Every page the agent reads passes through the scraper. Local means that traffic stays on my own server.
  • It fits the stack. Hermes already runs in a Docker container next to my other self-hosted services, so a local Firecrawl is one more folder in the same stack, not a new subscription.

The stack

Self-hosted Firecrawl is a Docker Compose stack, not a single container: the API on port 3002, a Playwright microservice that renders JavaScript-heavy pages, and supporting services for queues and storage (Redis, RabbitMQ, Postgres, FoundationDB). Together it sits around 4 GB of RAM on my server.

One environment variable matters most: with USE_DB_AUTHENTICATION=false, the API requires no key, so calls from my local network need no credentials. And I pinned the clone to a release tag instead of main: the compose contract changes between releases, and upstream verifies a specific release in its own self-hosting guide.

Setting it up

Clone the repo pinned to the release you want:

git clone --depth 1 --branch v2.11.162 https://github.com/firecrawl/firecrawl
cd firecrawl

Create a minimal .env (the template in their self-hosting guide lists many optional variables, but these three boot the stack):

PORT=3002
HOST=0.0.0.0
USE_DB_AUTHENTICATION=false

Build and start:

docker compose build
docker compose up -d

The default compose file builds the API, Playwright and Postgres images from source; prebuilt images exist on ghcr.io if you prefer to swap the build: lines for image: lines. The first start takes a while. Afterwards, test with a real scrape:

curl http://localhost:3002/v0/health/readiness
curl -X POST http://localhost:3002/v1/scrape \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com", "formats": ["markdown"]}'

The readiness endpoint only says the process is up; the scrape proves the workers actually return content.

Search: SearXNG next to the API

The self-hosted API exposes a POST /v2/search endpoint, but the cloud search backend (fire-engine) is not part of the self-hosted package, so search falls back to DuckDuckGo. That fallback rate-limits: during bursts of agent searches, my server IP got blocked (“Blocked by anti-bot measures”).

The fix is a SearXNG container next to the API: it fans each query out to many search engines at once. Two additions. A compose.override.yaml with the service and the endpoint variable on the API:

services:
  api:
    environment:
      SEARXNG_ENDPOINT: http://searxng:8080
  searxng:
    image: searxng/searxng:latest
    ports:
      - "127.0.0.1:8081:8080"
    volumes:
      - ./searxng:/etc/searxng

And a searxng/settings.yml enabling the JSON format the API consumes, with the limiter off since the caller is my own agent:

use_default_settings: true
server:
  secret_key: "<a random string>"
  limiter: false
search:
  formats:
    - html
    - json

With that, /v2/search tries SearXNG first and keeps DuckDuckGo as an automatic fallback when it returns nothing:

curl -X POST http://localhost:3002/v2/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "home assistant docker", "limit": 3}'

One Docker gotcha worth knowing: bind mounts resolve on the host filesystem. If the SearXNG container boots with the bundled template instead of your settings.yml, the file did not reach the host mount; write it on the host and recreate the container.

Pointing Hermes at it

Hermes picks Firecrawl from its environment file (~/.env):

FIRECRAWL_API_URL=http://172.20.0.1:3002
# FIRECRAWL_API_KEY=...  # commented out: when the URL is set, it wins

When FIRECRAWL_API_URL is set, Hermes talks to that URL and needs no API key; set only the key, and it goes to the cloud service. The web backend in Hermes’ config is set to firecrawl.

The address 172.20.0.1 is the Docker bridge gateway: Hermes runs in its own container, and localhost:3002 inside that container does not reach a port published on the host. If your agent runs directly on the host, http://localhost:3002 is enough.

What you don’t get

Self-hosted Firecrawl does not include fire-engine, Firecrawl’s anti-bot layer with proxies and block handling. Pages behind aggressive bot walls fail locally where the cloud service would pass. Screenshots and page actions are also cloud-only, and a few cloud-only response formats are rejected by the self-hosted API (the 400 error lists what your instance accepts).

The core paths all work: scraping to markdown, JavaScript rendering, crawling and search. And when a scrape really needs the cloud’s anti-bot layer, the fallback is the commented key in ~/.env: comment the URL line, uncomment the key, and Hermes is back on the cloud service.