---
title: "Deploy Firecrawl · Quick Start"
description: "Self-hosted web scraping and crawling API for LLMs, behind an API key"
category: "AI/ML"
url: https://railway.com/deploy/firecrawl-quick-start
---

# Deploy Firecrawl · Quick Start

Self-hosted web scraping and crawling API for LLMs, behind an API key

**[Deploy Firecrawl · Quick Start on Railway](https://railway.com/template/firecrawl-quick-start)**

Machine-readable deploy manifest (JSON, validated by TemplateCI): https://railway.com/deploy/firecrawl-quick-start/manifest.json

- **Creator:** withSeismic
- **Category:** AI/ML

## Template content

### playwright-service

- **Image:** ghcr.io/firecrawl/playwright-service:latest
- **Start command:** `node dist/api.js`
- **Health check:** /health

### redis

- **Image:** redis:alpine
- **Start command:** `/bin/sh -c "rm -rf $RAILWAY_VOLUME_MOUNT_PATH/lost+found/ && exec docker-entrypoint.sh redis-server --requirepass $REDIS_PASSWORD --save 60 1 --dir $RAILWAY_VOLUME_MOUNT_PATH"`

### nuq-postgres

- **Image:** ghcr.io/firecrawl/nuq-postgres:latest
- **Start command:** `docker-entrypoint.sh postgres -c max_wal_size=1GB -c min_wal_size=80MB -c cron.log_statement=off`

### api

- **Image:** ghcr.io/firecrawl/firecrawl:latest
- **Start command:** `node dist/src/index.js`
- **Health check:** /v0/health/liveness

### rabbitmq

- **Image:** rabbitmq:3-management

### gateway

- **Image:** caddy:latest
- **Start command:** `sh -c '[ -n "$FIRECRAWL_API_KEY" ] && [ -n "$BULL_AUTH_KEY" ] || { echo "gateway: FIRECRAWL_API_KEY and BULL_AUTH_KEY must be set" >&2; exit 1; }; printf "%s\n" "$CADDYFILE" > /etc/caddy/Caddyfile && exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile'`
- **Health check:** /healthz
- **Public domain:** Yes

### worker

- **Image:** ghcr.io/firecrawl/firecrawl:latest
- **Start command:** `bash -c 'exec > >(grep --line-buffered -v "\[nuq/metrics:") 2>&1; wait_for() { u="${1#*@}"; u="${u%%/*}"; u="${u%%\?*}"; h="${u%:*}"; p="${u##*:}"; i=0; until (exec 3<>"/dev/tcp/$h/$p") 2>/dev/null; do i=$((i+1)); if [ "$i" -ge 90 ]; then echo "worker: $h:$p is not reachable, giving up"; exit 1; fi; echo "worker: waiting for $h:$p"; sleep 2; done; }; wait_for "$NUQ_RABBITMQ_URL"; wait_for "$NUQ_DATABASE_URL"; n="${NUQ_WORKER_COUNT:-5}"; base="${NUQ_WORKER_START_PORT:-3006}"; NUQ_POD_NAME=worker WORKER_PORT="${WORKER_PORT:-3005}" node dist/src/services/queue-worker.js & NUQ_POD_NAME=extract-worker EXTRACT_WORKER_PORT="${EXTRACT_WORKER_PORT:-3004}" node dist/src/services/extract-worker.js & i=0; while [ "$i" -lt "$n" ]; do NUQ_POD_NAME="nuq-worker-$i" NUQ_WORKER_PORT="$((base+i))" node dist/src/services/worker/nuq-worker.js & i=$((i+1)); done; NUQ_POD_NAME=nuq-prefetch-worker-0 NUQ_PREFETCH_REPLICAS=1 NUQ_PREFETCH_WORKER_PORT="$((base+n))" node dist/src/services/worker/nuq-prefetch-worker.js & NUQ_POD_NAME=nuq-reconciler-worker-0 NUQ_RECONCILER_WORKER_PORT="$((base+n+1))" node dist/src/services/worker/nuq-reconciler-worker.js & pids="$(echo $(jobs -p))"; echo "worker: started processes $pids"; trap "echo worker: stopping; kill -TERM $pids 2>/dev/null; wait $pids; exit 0" TERM INT; wait -n $pids; echo "worker: a process exited, stopping the others so Railway restarts the service"; kill -TERM $pids 2>/dev/null; wait $pids; exit 1'`
- **Health check:** /health

## Documentation

# Deploy and Host Firecrawl on Railway

Firecrawl is an open-source API that turns websites into clean markdown and structured data for LLM applications. It scrapes single pages, crawls whole sites, maps a site's URLs and searches the web. With an LLM provider connected, it also extracts structured JSON. This template runs the official self-hosted stack from the Firecrawl repository and puts an API key in front of it, so your instance is not open to the internet.

## About Hosting Firecrawl

- **The upstream self-hosted stack.** It runs the same services as Firecrawl's `docker-compose.yaml`: `api`, `worker`, `playwright-service` (headless Chromium), `nuq-postgres` (the Postgres job queue), `redis` and `rabbitmq`. A small Caddy `gateway` sits in front of them.
- **Official images on moving tags.** `ghcr.io/firecrawl/firecrawl:latest`, `ghcr.io/firecrawl/playwright-service:latest`, `ghcr.io/firecrawl/nuq-postgres:latest`, `redis:alpine`, `rabbitmq:3-management` and `caddy:latest`. A redeploy picks up new releases.
- **An API key is required.** Self-hosted Firecrawl has no authentication of its own unless you run Supabase (`USE_DB_AUTHENTICATION=false` upstream). The gateway is the only public service. It answers any request without `Authorization: Bearer {FIRECRAWL_API_KEY}` with `401`. The key is generated when you deploy.
- **The queue admin UI is locked.** Bull Board is reachable only at `/admin/{BULL_AUTH_KEY}/queues`, and the 32-character key is generated at deploy time. Every other `/admin` path returns `404`, even when the request carries the API key.
- **No default passwords.** The Postgres, Redis and RabbitMQ passwords are generated. None of the data services has a public domain or a TCP proxy. All services talk over Railway's private network.
- **Durable.** Volumes hold Postgres (queue and crawl state), Redis and RabbitMQ data. Crawl results, keys and the admin UI survive a redeploy of every service.
- **A complete worker.** The `worker` service runs the same processes as upstream's all-in-one runner: the queue worker, the extract worker, five NuQ workers, the prefetch worker and the reconciler. If any one of them exits, the service restarts.
- **Healthchecks.** The gateway is checked at `/healthz`, the API at `/v0/health/liveness`, the worker at `/health` and Playwright at `/health`.
- **LLM keys are optional.** Scrape, crawl, map, batch scrape and search work without any key. Extract, the JSON format and summaries need an OpenAI-compatible key or an Ollama server.

### Getting started

1. Click Deploy. Leave the LLM fields empty if you only need scraping and crawling. The first boot takes a few minutes while the images are pulled.
2. Open the **gateway** service, then **Variables**. Copy `FIRECRAWL_API_URL` (your public URL) and `FIRECRAWL_API_KEY` (your key).
3. Check that the stack is up. This request needs no key:
   ```bash
   curl https://YOUR_DOMAIN/health
   # {"status":"ok"}
   ```
4. Scrape a page:
   ```bash
   curl -X POST https://YOUR_DOMAIN/v2/scrape \
     -H "Authorization: Bearer $FIRECRAWL_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"url":"https://example.com","formats":["markdown"]}'
   ```
5. Crawl a site. Crawls run on the worker. Start one, then poll it with the `id` it returns:
   ```bash
   curl -X POST https://YOUR_DOMAIN/v2/crawl \
     -H "Authorization: Bearer $FIRECRAWL_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"url":"https://example.com","limit":10}'

   curl https://YOUR_DOMAIN/v2/crawl/CRAWL_ID -H "Authorization: Bearer $FIRECRAWL_API_KEY"
   ```
6. Use the official SDKs. Point them at your URL and key:
   ```python
   # pip install firecrawl-py
   from firecrawl import Firecrawl

   app = Firecrawl(api_key="fc-...", api_url="https://YOUR_DOMAIN")
   doc = app.scrape("https://example.com", formats=["markdown"])
   print(doc.markdown)
   ```
   ```js
   // npm install @mendable/firecrawl-js
   import Firecrawl from "@mendable/firecrawl-js";

   const app = new Firecrawl({ apiKey: "fc-...", apiUrl: "https://YOUR_DOMAIN" });
   const doc = await app.scrape("https://example.com", { formats: ["markdown"] });
   console.log(doc.markdown);
   ```
7. Optional: open the queue admin UI at `https://YOUR_DOMAIN/admin/{BULL_AUTH_KEY}/queues`. The `BULL_AUTH_KEY` is on the **api** service.
8. Optional: to turn on extract, set `OPENAI_API_KEY` on the **api** service, or set `OLLAMA_BASE_URL` and `MODEL_NAME`. The worker reads both values from the api service. Then redeploy `api` and `worker`.

## Common Use Cases

- Turning web pages and documentation sites into markdown for retrieval-augmented generation (RAG) pipelines and knowledge bases.
- Giving AI agents a scraping and search backend. The official SDKs and the Firecrawl MCP server accept a self-hosted API URL and key.
- Crawling a site on a schedule and storing the pages, without per-page credits. You pay only for the Railway resources you use.
- Getting a site's URL list from its sitemap with `/v2/map` before you crawl it selectively.
- Extracting structured JSON from pages with your own OpenAI key or your own Ollama models.

## Dependencies for Firecrawl Hosting

- Firecrawl API and worker (`ghcr.io/firecrawl/firecrawl`)
- Playwright rendering service (`ghcr.io/firecrawl/playwright-service`)
- NuQ Postgres, Postgres 17 with pg_cron (`ghcr.io/firecrawl/nuq-postgres`)
- Redis and RabbitMQ
- Caddy, as the authenticating gateway

### Deployment Dependencies

- Firecrawl source: https://github.com/firecrawl/firecrawl
- Self-hosting guide: https://github.com/firecrawl/firecrawl/blob/main/SELF_HOST.md
- API reference: https://docs.firecrawl.dev/api-reference/introduction
- Container images: https://github.com/orgs/firecrawl/packages
- Firecrawl MCP server: https://github.com/firecrawl/firecrawl-mcp-server
- Caddy: https://caddyserver.com/docs/caddyfile
- Bull Board (the queue UI): https://github.com/felixmosh/bull-board
- Railway private networking: https://docs.railway.com/reference/private-networking

### Implementation Details

**How clients authenticate.** Every request to the public URL must carry the header `Authorization: Bearer {FIRECRAWL_API_KEY}`. This is the same header the Firecrawl SDKs send when you pass `api_key` / `apiKey`. The key must match exactly. A missing or wrong key gets `401` with a JSON error. Three things need no key: `GET /health` (the API's liveness check, for uptime monitors), `GET /healthz` (the gateway's own check) and CORS preflight `OPTIONS` requests. There is one shared key. This template does not provide per-user keys, usage tracking or credits.

**Admin routes.** The queue UI is at `/admin/{BULL_AUTH_KEY}/queues`. The `/admin/ops/*` routes need `Authorization: Bearer {BULL_AUTH_KEY}`. All other `/admin` paths return `404`.

| Service | Variable | Purpose |
|---|---|---|
| gateway | `FIRECRAWL_API_KEY` | The API key clients send. Generated as `fc-` plus 32 hex characters. |
| gateway | `FIRECRAWL_API_URL` | Your public base URL, for `api_url` / `apiUrl`. |
| gateway | `CADDYFILE` | Gateway rules: key check, admin lock, health routes. |
| api | `BULL_AUTH_KEY` | Queue admin key. Generated, 32 alphanumeric characters. |
| api | `OPENAI_API_KEY` | Optional. Enables extract, JSON format and summaries. |
| api | `OPENAI_BASE_URL` | Optional. An OpenAI-compatible endpoint. The default is `https://api.openai.com/v1`. |
| api | `MODEL_NAME` / `MODEL_EMBEDDING_NAME` | Optional model overrides. |
| api | `OLLAMA_BASE_URL` | Optional, for example `http://ollama.railway.internal:11434/api`. When set, Ollama becomes the default provider. Also set `MODEL_NAME`. |
| worker | `NUQ_WORKER_COUNT` | Number of parallel scrape job runners for crawls and batch scrapes. The default is 5. |
| playwright-service | `MAX_CONCURRENT_PAGES` | Number of browser pages open at once. The default is 10. |
| nuq-postgres, redis, rabbitmq | `POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `RABBITMQ_DEFAULT_PASS` | Generated passwords. They are used only on the private network. |

**Upgrade notes**

- The images track `:latest`. To upgrade, redeploy `api`, `worker` and `playwright-service`. Upstream sometimes changes the process layout. Before you upgrade an instance you depend on, check `SELF_HOST.md` and `docker-compose.yaml` in the release.
- The worker's start command runs the same processes as upstream's runner (`queue-worker`, `extract-worker`, `nuq-worker` × `NUQ_WORKER_COUNT`, `nuq-prefetch-worker`, `nuq-reconciler-worker`). If a release adds a process, add it to the start command.
- `nuq-postgres:latest` runs Postgres 17 today. If upstream moves it to a new major version, the existing data directory will not start. Before redeploying `nuq-postgres`, pin the tag, or dump and restore the data. The queue data is mostly short-lived job state.
- Postgres starts with `max_wal_size=1GB` and `min_wal_size=80MB`. These override the 16GB/4GB values that upstream's schema script sets, which could fill a small volume.
- To rotate the API key, change `FIRECRAWL_API_KEY` on the gateway and redeploy only the gateway. To rotate the admin key, change `BULL_AUTH_KEY` on the api and redeploy `api` and `gateway`. Keep both keys to letters, digits and dashes, because the gateway config uses them as literal values.
- Search uses Firecrawl's built-in DuckDuckGo fallback. To use your own SearXNG instance, set `SEARXNG_ENDPOINT` on the api.

## Why Deploy Firecrawl on Railway?

Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.

By deploying Firecrawl on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.


## Similar templates

- [Chat Chat](https://railway.com/deploy/-WWW5r) — Chat Chat, your own unified chat and search to AI platform.
- [Weaviate](https://railway.com/deploy/weaviate-1) — Vector database with hybrid search and an API key-protected API
- [Möbius Personal AI OS](https://railway.com/deploy/mobius-v01-personal-ai-os) — Self-hosted AI agent that builds and runs apps for you, with memory.

Open this page in a browser: https://railway.com/deploy/firecrawl-quick-start
