Mein Firecrawl-Stack lief bisher gegen einen Ollama-Server. Das war bequem, denn Ollama liefert Chat-Modell und Embedding-Modell unter einer einzigen Adresse aus. Seit ich meine zwei RTX A6000 auf vLLM umgestellt habe, ist dieser Komfort weg. Und genau daran bin ich beim ersten Versuch gescheitert.
Das Problem klingt banal, ist aber hartnäckig: vLLM serviert pro Instanz genau ein Modell, Firecrawl kennt aber nur eine einzige Base-URL. Ich brauche jedoch beide Modelle. Das Chat-Modell für die strukturierte Extraktion, das Embedding-Modell für das Link-Ranking beim Crawl.
In diesem Beitrag zeige ich Euch, wie ich das mit einem schlanken nginx-Router gelöst habe. Ohne eine einzige Zeile Firecrawl-Code zu patchen.
Warum der naheliegende Weg nicht funktioniert
Mein erster Versuch war der, den vermutlich jeder zuerst probiert: eine zweite Umgebungsvariable einführen. Also in der docker-compose.yaml ein OPENAI_EMBEDDING_BASE_URL ergänzen und fertig.
Das funktioniert nicht. Die Variable wird von Firecrawl schlicht nicht gelesen. Sie landet als unbenutzte Umgebungsvariable im Container und wird ignoriert. Schlimmer noch: Wer dafür die Zeile mit MODEL_EMBEDDING_NAME überschreibt, nimmt Firecrawl auch noch den Modellnamen weg.
Firecrawl reicht an die API nur diese vier Werte durch:
OPENAI_API_KEYOPENAI_BASE_URLMODEL_NAMEMODEL_EMBEDDING_NAME
Zwei Modellnamen, aber nur eine URL. Prüfen könnt Ihr das selbst in Eurem Checkout:
Befehl: grep -E 'OPENAI|MODEL' /opt/firecrawl/docker-compose.yaml
Bleibt also nur, die Aufteilung außerhalb von Firecrawl zu lösen. Genau dafür ist ein Reverse Proxy gemacht.
Die Zielarchitektur
Der Trick ist, dass Firecrawl weiterhin nur eine Adresse sieht. Dahinter verteilt nginx nach dem angefragten Pfad:
Firecrawl (192.168.2.119)
│
│ OPENAI_BASE_URL
▼
nginx-Router (192.168.2.25:8090)
│
┌─────────────┴─────────────┐
/v1/embeddings /v1/ (Rest)
│ │
▼ ▼
192.168.2.57:8001 192.168.2.57:8000
Qwen3-Embedding-4B Qwen3.8-27B-int4
GPU 0 GPU 1
Der Aufruf /v1/chat/completions landet also bei der Chat-Instanz, der Aufruf /v1/embeddings bei der Embedding-Instanz. Firecrawl merkt davon nichts.
Der nginx läuft bei mir auf einem separaten Host. Das ist kein Muss. Ihr könnt ihn genauso gut direkt auf dem vLLM-Host laufen lassen, dann spart Ihr einen Netzwerk-Hop. Ich nutze schlicht den, der ohnehin schon da ist und die Aufgabe hat für meine KI-Werkstatt das Routing zu übernehmen.
Phase 1: Die beiden vLLM-Instanzen prüfen
Bevor Ihr am Router bastelt, vergewissert Euch, dass beide Instanzen laufen und ihre Modellnamen ausliefern. Dazu auf dem vLLM-Host:
Befehl: curl http://127.0.0.1:8000/v1/models
Befehl: curl http://127.0.0.1:8001/v1/models
Bei mir kommen dabei diese beiden id-Werte zurück:
Frozenlock/Qwen3.8-27B-int4-AutoRound # Chat, Port 8000
qwen3-embedding-4b # Embedding, Port 8001
Schreibt Euch diese beiden Werte auf. Sie müssen später zeichengenau in die .env. vLLM antwortet sonst mit einem 404 und der Meldung, dass das Modell nicht existiert. Das ist der Fehler, den man am längsten sucht, weil er nach einem Netzwerkproblem aussieht und keines ist.
Der Test, den man gerne vergisst
Ein curl gegen 127.0.0.1 beweist nur, dass vLLM lokal antwortet. Ob die Instanz auch im LAN erreichbar ist, steht auf einem anderen Blatt:
Befehl: ss -tlnp | grep -E '8000|8001'
Steht dort 0.0.0.0:8000, ist alles gut. Steht dort 127.0.0.1:8000, lauscht vLLM nur lokal und der Router kommt nicht heran. Dann müsst Ihr die Instanz mit --host 0.0.0.0 neu starten.
Gegenprobe vom Router-Host aus:
Befehl: curl http://192.168.2.57:8000/v1/models
Befehl: curl http://192.168.2.57:8001/v1/models
Phase 2: nginx als Router konfigurieren
Jetzt der eigentliche Kern. Ihr geht auf den Rechner auf dem nginx bei euch installiert ist. Ich lege einen eigenen server-Block auf einem eigenen Port an, statt die Regeln in eine bestehende Site zu mischen. Das hält den Router unabhängig von allem anderen, was auf dem Host läuft.
Zuerst: Ist der Port überhaupt frei?
Diesen Schritt habe ich beim ersten Anlauf übersprungen, und er hat mich eine halbe Stunde gekostet. Ich wollte den Router auf Port 8080 legen. Dort saß aber längst mein AdGuard Home in einem Docker-Container. Also prüft das vorher:
Befehl: sudo ss -tlnp | grep :8090
Kommt keine Ausgabe zurück, ist der Port frei. Kommt eine Zeile, nehmt Ihr einen anderen. Ich habe mich für 8090 entschieden, weil 8080 auf meinem Host belegt war.
Warum das so tückisch ist: nginx -t prüft nur die Syntax, nicht die Verfügbarkeit des Ports. Der Test läuft also sauber durch, der Reload meldet Erfolg, und trotzdem ist Euer Router nicht da. Die Wahrheit steht dann nur im nginx-eigenen Log:
bind() to 0.0.0.0:8080 failed (98: Address already in use)
still could not bind()
Der alte Master-Prozess läuft in diesem Fall einfach mit der alten Konfiguration weiter. Eure Anfragen landen beim anderen Dienst, der auf /v1/models schlicht nichts zurückgibt. Es sieht aus wie ein Routing-Problem, ist aber ein Portkonflikt.
Die Konfiguration anlegen
Befehl: sudo nano /etc/nginx/sites-available/vllm-router
server {
listen 8090;
# Embeddings gehen an die zweite vLLM-Instanz
location /v1/embeddings {
proxy_pass http://192.168.2.57:8001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
# Alles andere an die Chat-Instanz
location /v1/ {
proxy_pass http://192.168.2.57:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
Drei Details in dieser Konfiguration sind wichtig, und alle drei sind Stolpersteine:
- Die Reihenfolge spielt keine Rolle. nginx wählt immer den längsten passenden Prefix. Deshalb gewinnt
/v1/embeddingsgegen/v1/, egal in welcher Reihenfolge die Blöcke stehen. - Kein Pfad hinter
proxy_pass. Die Zeile endet bewusst mit dem Port. Hängt dort ein Slash oder ein Pfad, schreibt nginx die URL um und vLLM bekommt einen Pfad, den es nicht kennt. - Großzügige Timeouts. Ein 27B-Modell braucht bei langen Seiten deutlich länger als die nginx-Standardminute. Ohne
proxy_read_timeoutbricht der Router mitten in der Antwort ab. Undproxy_buffering offsorgt dafür, dass Streaming-Antworten durchlaufen, statt gesammelt zu werden.
Aktivieren, prüfen und starten:
Befehl: sudo ln -s /etc/nginx/sites-available/vllm-router /etc/nginx/sites-enabled/
Befehl: sudo nginx -t
Befehl: sudo systemctl restart nginx
Beachtet, dass hier restart steht und nicht reload. Nur beim Neustart übernimmt der Master-Prozess eine geänderte listen-Direktive zuverlässig.
Und dann der Beweis, dass es diesmal wirklich nginx ist, der auf dem Port sitzt:
Befehl: sudo ss -tlnp | grep :8090
In der Ausgabe muss nginx als Prozess stehen. Steht dort etwas anderes oder gar nichts, schaut in /var/log/nginx/error.log.
Falls Ihr wie ich einen anderen Dienst auf dem ursprünglich geplanten Port habt, prüft zum Schluss noch, ob der weiterhin läuft:
Befehl: curl -I http://192.168.2.25:8080/
Phase 3: Den Router testen
Jetzt prüft Ihr beide Pfade einzeln, und zwar vom Firecrawl-Host aus. Nur so testet Ihr gleichzeitig die Netzwerk-Erreichbarkeit.
Zuerst der Chat-Pfad:
Befehl: curl http://192.168.2.25:8090/v1/models
Erwartet wird die Modellliste Eurer Chat-Instanz. Dass hier nur das Chat-Modell auftaucht und nicht das Embedding-Modell, ist korrekt und kein Fehler. /v1/models fällt ja unter die allgemeine Regel.
Dann der Embedding-Pfad:
curl -X POST http://192.168.2.25:8090/v1/embeddings \
-H 'Content-Type: application/json' \
-d '{"model": "qwen3-embedding-4b", "input": "Ein kurzer Testsatz."}' | head -c 300
Erwartet wird ein JSON mit einem langen Zahlenarray unter data[0].embedding. Das head -c 300 hängt dran, weil so ein Vektor sonst die halbe Konsole flutet. Kommt hier eine Fehlermeldung über ein unbekanntes Modell, stimmt der Name nicht. Kommt gar nichts, greift die Routing-Regel nicht oder der Port ist belegt.
Und zur Sicherheit noch der Chat-Pfad mit einer echten Anfrage:
curl -X POST http://192.168.2.25:8090/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "Frozenlock/Qwen3.8-27B-int4-AutoRound",
"messages": [{"role": "user", "content": "Sag Hallo."}],
"max_tokens": 20
}'
Erst wenn beide Pfade sauber antworten, geht es an Firecrawl. Fehler an dieser Stelle zu suchen ist deutlich angenehmer, als sie später in den Container-Logs zu jagen.
Phase 4: Firecrawl umstellen
Der eigentliche Umbau an Firecrawl ist erfreulich klein. Es ändern sich genau vier Zeilen in der .env. An der docker-compose.yaml ändert sich nichts.
Befehl: cp /opt/firecrawl/.env /opt/firecrawl/.env.ollama
Befehl: nano /opt/firecrawl/.env
# ===== vLLM via nginx-Router =====
OPENAI_BASE_URL=http://192.168.2.25:8090/v1
OPENAI_API_KEY=vllm
MODEL_NAME=Frozenlock/Qwen3.8-27B-int4-AutoRound
MODEL_EMBEDDING_NAME=qwen3-embedding-4b
Dazu vier Anmerkungen:
- Der Pfad
/v1gehört ans Ende der Base-URL, genau wie bei Ollama. - Der
OPENAI_API_KEYmuss gesetzt sein, darf aber jeder beliebige String sein. Habt Ihr vLLM mit--api-keygestartet, tragt Ihr natürlich den echten Key ein. - Die Modellnamen übernehmt Ihr exakt so, wie sie aus
/v1/modelskamen. Inklusive Schrägstrich und Groß- und Kleinschreibung. - Ein eventuell vorhandenes
OLLAMA_BASE_URLkönnt Ihr löschen. Die Variable wird ohnehin nicht ausgewertet.
Anschließend den Stack neu starten:
Befehl: sudo systemctl restart firecrawl
Befehl: cd /opt/firecrawl && docker compose ps
Erwartet werden wieder fünf laufende Container und die API auf 0.0.0.0:3002.
Phase 5: Der Funktionstest
Erst warten, dann testen
Ein Fehler, in den ich prompt hineingelaufen bin: Ich habe sofort nach dem Neustart getestet und bekam ein curl: (56) Recv failure: Connection reset by peer. Das sah nach einem kaputten Setup aus, war aber nur Ungeduld. Der API-Container war gerade drei Sekunden alt und noch nicht bereit.
Wartet deshalb auf das Lebenszeichen, bevor Ihr weitermacht:
Befehl: curl http://localhost:3002/
Erst wenn dort {"message":"Firecrawl API", ...} zurückkommt, hat es Sinn weiterzumachen.
Ein Hinweis zu den Logs an dieser Stelle: Direkt nach dem Neustart taucht dort oft eine RabbitMQ-Meldung mit noproc und falling back to postgres auf. Das ist harmlos. Ein Worker greift auf eine Queue zu, die gerade erst neu angelegt wird, und Firecrawl weicht automatisch auf Postgres aus.
Der einfache Scrape
Zuerst der Scrape, der ganz ohne KI auskommt:
curl -X POST http://localhost:3002/v2/scrape \
-H 'Content-Type: application/json' \
-d '{"url": "https://ai-box.eu", "formats": ["markdown"]}' | head -c 300
Der beweist nur, dass Firecrawl selbst läuft. An Eurem vLLM-Server kommt dabei nichts an.
Der strukturierte Scrape
Das ist der eigentliche Test, denn erst hier wird das Chat-Modell über den Router angefragt:
curl -X POST http://localhost:3002/v2/scrape \
-H 'Content-Type: application/json' \
-d '{
"url": "https://ai-box.eu",
"formats": [{
"type": "json",
"schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"main_topics": {"type": "array", "items": {"type": "string"}}
}
}
}]
}'
Das dauert spürbar länger als der Markdown-Scrape, weil das Modell die komplette Seite verarbeiten muss. Kommt am Ende ein sauberes JSON mit Titel und Themen zurück, ist Eure vLLM-Anbindung fertig.
Bei Fehlern hilft der Blick in die Logs:
Befehl: docker compose logs api --tail=50
Mein Lieblingswerkzeug an dieser Stelle ist aber ein zweites Terminal auf dem vLLM-Host:
Befehl: watch -n 1 nvidia-smi
Wenn die GPU-Auslastung während des Scrapes anspringt, wisst Ihr sofort, dass die Anfrage wirklich durchkommt. Das ist aussagekräftiger und schneller als jedes Log.
Typische Fehler und was dahintersteckt
- Leere Antwort, kein Fehler, kein Timeout: Der Port ist von einem anderen Dienst belegt und nginx konnte nicht binden. Prüft
ss -tlnpund/var/log/nginx/error.log. Das ist der Fehler, dennginx -tnicht findet. - Recv failure: Connection reset by peer: Der API-Container war noch nicht bereit. Wartet auf
curl http://localhost:3002/und testet erneut. - 404 mit „model not found“: Der Wert in
MODEL_NAMEstimmt nicht mit deridaus/v1/modelsüberein. Häufigster Fehler, und der ärgerlichste, weil er nach einem Verbindungsproblem aussieht. - Connection refused: vLLM lauscht nur auf
127.0.0.1. Zurück zu Phase 1 und die Instanzen mit--host 0.0.0.0starten. - 504 Gateway Timeout: Die Timeouts im nginx fehlen oder sind zu knapp. Das Modell braucht länger als die Standardminute.
- Embeddings landen beim Chat-Modell: Der
location-Block für/v1/embeddingsfehlt oder hat einen Tippfehler. Testet den Pfad einzeln mit demcurlaus Phase 3. - Leere Antwort trotz laufendem nginx: Meist ein Pfad hinter
proxy_pass. Die Zeile muss mit dem Port enden.
Die Kurzfassung, falls Ihr keine Embeddings braucht
Ehrlich gesagt: Nicht jeder braucht diesen Aufbau. Das Embedding-Modell nutzt Firecrawl für das semantische Ranking von Links, also beim themengefilterten Crawl über viele Seiten. Wer Firecrawl hauptsächlich mit scrape nutzt und die Analyse ohnehin im eigenen Agenten macht, merkt vom fehlenden Ranking nichts.
In diesem Fall lasst Ihr den Router einfach weg und zeigt direkt auf die Chat-Instanz:
OPENAI_BASE_URL=http://192.168.2.57:8000/v1
OPENAI_API_KEY=vllm
MODEL_NAME=Frozenlock/Qwen3.8-27B-int4-AutoRound
Extract, Agent und die strukturierte Extraktion funktionieren damit vollständig. Nur das Relevanz-Ranking beim Crawl entfällt.
Mein persönliches Fazit
Was mich an dieser Umstellung am meisten gekostet hat, war nicht die Technik, sondern eine falsche Annahme. Ich war überzeugt, dass sich das Problem mit einer zusätzlichen Umgebungsvariable lösen lassen muss. Also habe ich eine erfunden, sie in die Compose-Datei geschrieben und mich gewundert, warum nichts passiert. Diese Änderung hat mir dann Wochen später beim Update von Firecrawl einen Merge-Konflikt beschert, den ich erst einmal verstehen musste.
Die Lehre daraus ist für mich zweigeteilt. Erstens: Wenn eine Software eine Fähigkeit nicht hat, dann löst man das davor und nicht darin. Ein Reverse Proxy ist genau dafür da, und die Lösung besteht am Ende aus 20 Zeilen Konfiguration. Zweitens, und das ist mir fast wichtiger: Änderungen am Upstream-Code rächen sich beim nächsten Update. Alles, was ich außerhalb des Repositories lösen kann, löse ich auch außerhalb.
Eine dritte Lehre kam beim Aufbau noch dazu, und die war für mich die überraschendste: Ein erfolgreicher Test bedeutet nicht, dass es läuft. Weder sagt nginx -t etwas über belegte Ports, noch heißt ein gestarteter Container, dass die API schon antwortet. Beide Male habe ich den Fehler zuerst an der falschen Stelle gesucht. Ein kurzes ss -tlnp und ein curl auf den Wurzelpfad ersparen einem viel Grübelei.
Der Nebeneffekt gefällt mir inzwischen sogar besser als die eigentliche Lösung. Ich habe jetzt einen einzigen Endpunkt im LAN, hinter dem beide Modelle hängen. Den nutzt nicht nur Firecrawl, sondern auch mein Hermes-Agent. Und wenn ich morgen ein drittes Modell dazustelle, ist das ein weiterer location-Block und sonst nichts.






Ein toller Guide der leicht zugänglich und verständlich ist. Perfekt für ein kleines Side-Project geeignet. Aktuell half mir noch mein…
Thank you for this great tutorial, could you share n8n workflow and comfyui workflow please?
Hallo Anton, die Meldung besagt das in meinem Beisiel Methoden verwendet werden die veraltet (deprecated) sind. Also müsstest Du die…
Danke für das Tool! Ich habe erst kürzlich angefangen mich mit der Thematik zu beschäftigen und bin für meine Erwartungen…
Hallo, ich habe ihre Anleitung befolgt und bekomme im letzten Schritt leider immer folgende Meldung im Terminal: bash <(wget -qO-…