Open source. This crawler is now public at codeberg.org/cl0qsearch/crawler-node — run a node, help index the open web. More to come.
cl0q is a free, independent search engine with its own crawler and index. This repository is that crawler, packaged so other people can run a node: it discovers sites, fetches their homepages politely, and sends what it finds to cl0q's index. The search engine itself is not part of this repository (see cl0q-search).
cp .env.example .env # fill in DB_PASSWORD
docker compose build
docker compose run --rm --entrypoint /usr/local/bin/cl0q-node node enroll --contact you@example.com # one-command enrol
docker compose run --rm preflight # self-check: all six must PASS
docker compose up -d
docker compose logs -f website-crawler nodeSee docs/NODE_OPERATORS.md for the full operator guide: the 5-minute quickstart, what data a node sends (and never sends), and stop/update/uninstall.
seeds.txt ─▶ domain-crawler ─▶ domain_info ─▶ dns-enricher / asn-enricher
│
▼
website-crawler ─▶ website_info ─▶ cl0q-node ─▶ https://cl0q.com/api/ingest
(local Postgres) (token + enrolled IP)
| Service | What it does |
|---|---|
domain-crawler |
Follows links from seeds.txt and from known domains to discover new domains. |
website-crawler |
Fetches each domain's homepage and records its title, description, headings, language, links and response details. |
dns-enricher |
Resolves IP, MX and CNAME records for new domains (DNS only). |
asn-enricher |
Looks up each site's network (ASN) so requests are spread across hosting providers. Optional. |
cl0q-node |
Sends new rows to cl0q with your node's token. |
Everything runs against the node's own Postgres. Nothing listens on a public port.
These rules are built into the code. None of them can be switched off, and settings can only make a node slower or gentler than the limits below, never faster. They're what cl0q.com/bot promises site owners.
- One honest User-Agent, naming your node:
Mozilla/5.0 (compatible; cl0q-node/1.0; operator=<your-node-id>; +https://cl0q.com/bot). It never imitates a browser and can't be changed. - Public internet only. A node never connects to loopback, private networks, link-local addresses (including cloud metadata at 169.254.169.254) or other reserved ranges. The check runs on every connection, after DNS and on every redirect, so a page can't steer a node into its own network.
- robots.txt first, always. A group for
cl0q-nodeor any other cl0q bot name (cl0q-crawler,cl0q-discovery,cl0q-favicon,cl0q) takes precedence over*. Path rules andCrawl-delay(capped at 30 s) are obeyed. - Paced per server.
website-crawlerleaves at least 1 second between requests to any server IP and any site — robots.txt, http fallbacks, redirect hops and retries included.domain-crawlerleaves at least 2 seconds between requests to the same site. - Redirects stay on the site. At most 5 hops, each checked against that host's robots.txt and paced like any other request. A redirect to a different site is not followed; that site is queued and crawled on its own terms.
- Stop when refused. A dropped connection, reset or timeout counts as a refusal and isn't retried in the same
cycle. A 429/503 is retried once only if its
Retry-Afteris 15 s or less; a longer one is respected by leaving the site alone until a later cycle. Repeated refusals pause the whole network or IP group for 1 minute, doubling up to 1 hour.http://is tried only when a site doesn't offerhttps://at all. - Small footprint.
website-crawlerfetches only homepages, at most once per 120 days.domain-crawlerfetches at most 25 pages per site per cycle. It never fetches login, admin, account, cart, search or query-string URLs, or non-HTML files. Pages are capped at 5 MB. - Challenge pages aren't kept. Cloudflare, Imperva, DataDome and similar interstitials are detected, their content is thrown away, and they're never sent to cl0q.
You need Docker with Compose v2 and a server whose provider allows crawling. A small VPS (1 vCPU, 2 GB RAM) is enough. Avoid a home connection: abuse reports from site owners go to the owner of the IP address.
- Enroll. Email us the public IP address your node will crawl from. You'll get a node id and a token. The token only works from that IP.
- Configure.
cp .env.example .env # set CL0Q_NODE_ID, NODE_TOKEN and DB_PASSWORD (and IPINFO_TOKEN if you have one) - Start.
docker compose up -d --build docker compose logs -f website-crawler node
- Stop with
docker compose down. Each service finishes the work in hand and writes it before exiting, so nothing is lost. Your local data stays in thepgdatavolume. Add-vto delete it.
See docs/CRAWLER_NODES.md for the trust model, what is sent to cl0q, and what's expected of node operators.
A full node needs enrollment (node id + token, see above). Without them you can still verify a fresh checkout builds and the database schema applies:
cp .env.example .env
# dummy values for the build-only check (a real run needs your enrolled id + token):
# CL0Q_NODE_ID=smoke-test NODE_TOKEN=dummy DB_PASSWORD=smoke
docker compose config # validates variable interpolation, exits 0
docker compose up -d --build db # local Postgres only
docker compose exec db psql -U cl0q -d cl0q -c '\dt'
# -> tables (website_info, domain_info, ...) — initdb/01-schema.sql applied on first start
docker compose down # add -v to wipe the pgdata volume| Variable | Default | Meaning |
|---|---|---|
CL0Q_NODE_ID |
— (required) | Your node id; sent in every User-Agent. |
NODE_TOKEN |
— (required) | Ingest token from enrollment. |
DB_PASSWORD |
— (required) | Password for the node's local Postgres. |
INGEST_URL |
https://cl0q.com |
Where results are sent. Must be https: the token goes with every request. |
IPINFO_TOKEN |
empty | Free ipinfo Lite token for ASN grouping. |
CRAWL_WORKERS |
8 |
Concurrent homepage fetches. The per-server pacing always applies. |
TARGET |
empty | One-off/debug: crawl a single bare domain (e.g. example.com) instead of the frontier. |
SEED_URL |
empty | Comma-separated start URLs for domain-crawler; overrides -seeds-file and the built-in defaults. |
FRONTIER_SAMPLE_PCT |
0.5 |
TABLESAMPLE % for the uncrawled-frontier draw (website-crawler); keeps the batch representative across ASNs. |
LOOP |
empty | If set (and not 0), asn-enricher runs continuously instead of once. |
SPOOL_DATABASE_URL |
empty | cl0q's own deployment only — node operators leave this unset. |
Everything else is optional and listed in .env.example. Politeness settings are floors: a value below a limit
(including zero or a negative number) is raised to it, so they can only make a node gentler. Throughput settings
(workers, connections, timeouts) are kept within sane ranges the same way.
If cl0q's ingest rejects a batch outright (anything but an auth, rate-limit or server error), cl0q-node keeps it in
the local cl0q_node_deadletter table and carries on with the rows after it; please tell us if that table fills up.
go vet ./... && go test ./...
# a local Postgres with initdb/01-schema.sql applied, then e.g. (TARGET is a bare domain; homepages only):
CL0Q_NODE_ID=dev-node DATABASE_URL=postgres://cl0q:cl0q@localhost:5432/cl0q?sslmode=disable \
TARGET=example.com go run ./cmd/website-crawlerTest servers on 127.0.0.1 are refused by the address guard, by design. The package tests switch the guard off
inside the test binary only; there is no flag or environment variable for it.
cmd/<service>/ one directory per binary
botid/ the User-Agent and robots.txt names (one place, not configurable)
netguard/ refuses connections to non-public addresses
robotstxt/ the robots.txt rules both crawlers obey
crawlutil/ helpers both crawlers share (registrable site via the public suffix list, https fallback rule)
dbresil/ database-outage helpers (wait for the DB, connect timeouts)
initdb/ the node's database schema
seeds.txt starting points for discovery
- cl0q-search — the search backend that reads the index this node fills.
- robotstxt — vendored here as
robotstxt/; the robots.txt parser both crawlers obey. - botid — vendored here as
botid/; the User-Agent and robots.txt names (one place, not configurable). - netguard — vendored here as
netguard/; refuses connections to non-public addresses.