DevOps & Skalierung

Docker + CaptchaAI: Containerisierte CAPTCHA-Lösung

Ein CAPTCHA-Solver läuft dann zuverlässig in Produktion, wenn seine Umgebung überall identisch ist – auf dem Laptop im Entwicklerteam genauso wie auf dem Hetzner-Server im Rechenzentrum. Genau das leistet ein Docker-Container: Er bündelt Python-Laufzeit, Abhängigkeiten und Solver-Code in ein reproduzierbares Image, das Sie versionieren, in eine Registry pushen und beliebig oft neu starten können. Dieser Leitfaden führt Schritt für Schritt vom minimalen Dockerfile über einen schlanken Multi-Stage-Build bis zu einem Docker-Compose-Setup, das mehrere Solver-Worker parallel betreibt.


Das Basis-Dockerfile

Der Einstieg ist bewusst minimal: ein schlankes python:3.11-slim-Image, die Abhängigkeiten aus requirements.txt und das Solver-Skript. Entscheidend ist die letzte Design-Regel – der API-Schlüssel wird nicht in das Image gebacken, sondern zur Laufzeit als Umgebungsvariable übergeben. Die Zeile ENV CAPTCHAAI_KEY="" legt lediglich einen leeren Platzhalter an, den docker run später füllt.

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY solver.py .

# API key passed at runtime, not baked into image
ENV CAPTCHAAI_KEY=""

CMD ["python", "solver.py"]

requirements.txt:

requests>=2.31.0

Mehr braucht der Basis-Solver nicht: Die gesamte Kommunikation mit CaptchaAI läuft über HTTP-Requests, also genügt die requests-Bibliothek.


Das Solver-Skript

Das Skript kapselt den kompletten Ablauf einer Lösung: Es übermittelt die Aufgabe an in.php, fragt anschließend im Sekundentakt den Status über res.php ab und gibt das fertige Token zurück. Das Polling ist auf 24 Durchläufe à 5 Sekunden begrenzt, also maximal 120 Sekunden – passend zur Lebensdauer eines reCAPTCHA-Tokens. Alle Zugangsdaten kommen ausschließlich aus Umgebungsvariablen, damit kein Geheimnis im Image landet.

# solver.py
import os
import sys
import requests
import time


def solve_recaptcha(api_key, site_key, page_url):
    """Solve reCAPTCHA v2 using CaptchaAI."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key,
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1,
    }, timeout=30)
    result = resp.json()

    if result.get("status") != 1:
        raise RuntimeError(f"Submit error: {result.get('request')}")

    task_id = result["request"]

    # Poll for result
    for _ in range(24):  # 120s max
        time.sleep(5)
        resp = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key,
            "action": "get",
            "id": task_id,
            "json": 1,
        }, timeout=15)
        data = resp.json()
        if data["request"] != "CAPCHA_NOT_READY":
            if data.get("status") == 1:
                return data["request"]
            raise RuntimeError(f"Solve error: {data['request']}")

    raise TimeoutError("Solve timeout")


if __name__ == "__main__":
    api_key = os.environ.get("CAPTCHAAI_KEY")
    if not api_key:
        print("Error: CAPTCHAAI_KEY environment variable required")
        sys.exit(1)

    site_key = os.environ.get("SITE_KEY", "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-")
    page_url = os.environ.get("PAGE_URL", "https://example.com")

    token = solve_recaptcha(api_key, site_key, page_url)
    print(f"Token: {token[:50]}...")

Dasselbe Muster – übermitteln, Status abfragen, Token zurückgeben – gilt für alle unterstützten CAPTCHA-Typen. Für reCAPTCHA v2 tauschen Sie nur den method-Wert, für Cloudflare Turnstile oder GeeTest v3 die jeweiligen Parameter.


Image bauen und ausführen

Der Build erzeugt ein benanntes Image, der Lauf reicht die drei Umgebungsvariablen hinein. --rm entfernt den Container nach dem Durchlauf automatisch – ideal für einen einmaligen Test.

# Build
docker build -t captchaai-solver .

# Run with API key from environment
docker run --rm \
  -e CAPTCHAAI_KEY="YOUR_API_KEY" \
  -e SITE_KEY="TARGET_SITE_KEY" \
  -e PAGE_URL="https://example.com" \
  captchaai-solver

Ersetzen Sie YOUR_API_KEY durch Ihren echten Schlüssel aus dem Dashboard und TARGET_SITE_KEY durch den Sitekey der Zielseite. --rm sorgt dafür, dass nach dem Testlauf kein verwaister Container zurückbleibt.


Multi-Stage-Build für die Produktion

Für den Produktivbetrieb lohnt sich ein Multi-Stage-Build: Die Build-Phase installiert die Abhängigkeiten, die Runtime-Phase kopiert nur das fertige Ergebnis. Das Image bleibt schlank, und der Container läuft nicht als root, sondern unter einem eigenen Benutzer solver – ein einfacher, aber wirkungsvoller Härtungsschritt.

# Build stage
FROM python:3.11-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt

# Runtime stage
FROM python:3.11-slim

# Run as non-root
RUN useradd --create-home solver
USER solver

WORKDIR /home/solver/app

COPY --from=builder /app/deps /home/solver/app/deps
COPY solver.py .

ENV PYTHONPATH=/home/solver/app/deps
ENV PYTHONUNBUFFERED=1

CMD ["python", "solver.py"]

PYTHONUNBUFFERED=1 sorgt dafür, dass Log-Ausgaben sofort erscheinen – wichtig, damit docker logs und Log-Aggregatoren in Echtzeit mitlesen.


Docker Compose: mehrere Worker parallel

Sobald das Volumen steigt, verteilen Sie die Last über mehrere Container. Docker Compose beschreibt das gesamte Setup deklarativ – hier vier Solver-Worker, ein Redis als Aufgaben-Queue und vier Queue-Worker, die Aufgaben aus Redis ziehen.

# docker-compose.yml
version: "3.8"

services:
  solver-worker:
    build: .
    environment:

      - CAPTCHAAI_KEY=${CAPTCHAAI_KEY}
    restart: unless-stopped
    deploy:
      replicas: 4
      resources:
        limits:
          memory: 256M
          cpus: "0.25"

  redis:
    image: redis:7-alpine
    ports:

      - "6379:6379"

  queue-worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    environment:

      - CAPTCHAAI_KEY=${CAPTCHAAI_KEY}
      - REDIS_URL=redis://redis:6379
    depends_on:

      - redis
    deploy:
      replicas: 4

Ein wichtiger Punkt zur Abrechnung: Die Anzahl der Container ist nicht dasselbe wie die Anzahl paralleler Lösungen, die Ihr CaptchaAI-Tarif zulässt. CaptchaAI rechnet pro Thread ab – ein Thread ist eine gleichzeitig laufende Lösung, danach ist er sofort wieder frei. Der Tarif BASIC (15 $/Monat, 5 Threads) erlaubt fünf parallele Lösungen, STANDARD (30 $/Monat, 15 Threads) fünfzehn und ADVANCE (90 $/Monat, 50 Threads) fünfzig – jeweils mit unbegrenzten Lösungen pro Thread. Mehr Container als verfügbare Threads bringen keinen zusätzlichen Durchsatz; skalieren Sie also die Replikate an Ihrer Thread-Zahl aus.


Warteschlangen-Worker mit Redis

Der Queue-Worker blockiert an blpop auf der Redis-Liste captcha:tasks, verarbeitet jede eingehende Aufgabe generisch (der method-Wert kommt aus der Task-Payload) und schreibt das Ergebnis nach captcha:results zurück. So entkoppeln Sie das Einreichen von Aufgaben vom Lösen – Ihre Anwendung legt Aufgaben in Redis ab, die Worker arbeiten sie unabhängig davon ab.

# queue_worker.py
import os
import json
import time
import redis
import requests


def process_task(api_key, task_data):
    """Process a single CAPTCHA task from the queue."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key,
        "method": task_data["method"],
        "json": 1,
        **task_data["params"],
    }, timeout=30)
    result = resp.json()

    if result.get("status") != 1:
        return {"error": result.get("request")}

    task_id = result["request"]

    for _ in range(24):
        time.sleep(5)
        resp = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15)
        data = resp.json()
        if data["request"] != "CAPCHA_NOT_READY":
            if data.get("status") == 1:
                return {"token": data["request"]}
            return {"error": data["request"]}

    return {"error": "timeout"}


def main():
    api_key = os.environ["CAPTCHAAI_KEY"]
    redis_url = os.environ.get("REDIS_URL", "redis://localhost:6379")
    r = redis.from_url(redis_url)

    print("Worker started, waiting for tasks...")
    while True:
        _, raw = r.blpop("captcha:tasks")
        task = json.loads(raw)
        task_id = task.get("id", "unknown")

        print(f"Processing task {task_id}...")
        result = process_task(api_key, task)

        r.hset("captcha:results", task_id, json.dumps(result))
        print(f"Task {task_id} done: {'ok' if 'token' in result else 'error'}")


if __name__ == "__main__":
    main()

Da jeder Worker während des Pollings überwiegend wartet, kann ein einzelner Prozess mehrere Aufgaben nacheinander abarbeiten, ohne die CPU auszulasten. Der begrenzende Faktor bleibt die Thread-Zahl Ihres Tarifs, nicht die Container-Ressourcen.


Umgebungsvariablen und Secrets sicher verwalten

Der API-Schlüssel gehört nie ins Repository. Legen Sie ihn in einer .env-Datei ab, die per .gitignore ausgeschlossen ist, und reichen Sie sie an Compose weiter. Über --scale passen Sie die Worker-Zahl im laufenden Betrieb an.

# .env file (never commit to Git)
CAPTCHAAI_KEY=your_api_key_here

# .gitignore
echo ".env" >> .gitignore

# Run with .env file
docker compose --env-file .env up -d

# Scale workers
docker compose up -d --scale queue-worker=8

In einer GitLab-CI- oder GitHub-Actions-Pipeline hinterlegen Sie den Schlüssel stattdessen als maskierte CI-Variable oder als Repository-Secret – die .env-Datei bleibt der lokalen Entwicklung vorbehalten. Für Cluster-Betrieb setzen Docker Swarm und Kubernetes auf echte Secret-Objekte (siehe FAQ).


Praxisbeispiel: Scraping-Worker auf Hetzner

Ein typisches DACH-Setup: Ein Datenextraktions-Dienst läuft auf einem Hetzner-Cloud-Server (CX-Reihe), die CI/CD-Pipeline in GitLab baut das Solver-Image bei jedem Merge und pusht es in die GitLab Container Registry. Auf dem Server zieht docker compose pull die neue Version, docker compose up -d startet die Worker ohne Ausfall neu. Der CAPTCHAAI_KEY liegt als geschützte CI-Variable vor und wird nur zur Laufzeit injiziert.

Bei Scraping-Workflows sollten Sie zusätzlich die datenschutzrechtliche Seite prüfen: IP-Adressen und andere personenbezogene Daten fallen unter die DSGVO. Klären Sie Rechtsgrundlage und Datenflüsse für Ihren konkreten Anwendungsfall – die Containerisierung ändert nichts an dieser Verantwortung, macht aber wenigstens die technische Umgebung nachvollziehbar und reproduzierbar.


Fehlerbehebung

Problem Ursache Lösung
Container beendet sich sofort CAPTCHAAI_KEY fehlt -e CAPTCHAAI_KEY=... übergeben
DNS-Auflösung schlägt fehl Kein Netzwerkzugriff Docker-Netzwerkeinstellungen prüfen
Hoher Speicherverbrauch Zu viele gleichzeitige Anfragen Container-Speicher und Parallelität begrenzen
API-Schlüssel im Image sichtbar Schlüssel im Dockerfile hinterlegt Umgebungsvariablen oder Secrets verwenden

Häufige Fragen

Welches Basis-Image eignet sich für einen CAPTCHA-Solver?

python:3.11-slim. Es enthält eine vollständige Python-Laufzeit bei deutlich kleinerer Größe als das Standard-Image. Ein reiner HTTP-Solver braucht keine Systempakete darüber hinaus – alpine spart zwar weitere Megabyte, führt aber gelegentlich zu Kompatibilitätsproblemen bei vorkompilierten Wheels.

Wie viel Arbeitsspeicher braucht ein Solver-Container?

Sehr wenig. Ein Worker, der nur HTTP-Requests stellt und auf Antworten wartet, kommt komfortabel mit dem im Compose-Beispiel gesetzten Limit von 256 MB aus. Der Engpass ist praktisch nie der Speicher, sondern die Thread-Zahl Ihres CaptchaAI-Tarifs.

Läuft derselbe Container auch in Kubernetes?

Ja. Das Image ist umgebungsunabhängig – dieselbe Definition läuft lokal, in Docker Compose und in Kubernetes. In Kubernetes übergeben Sie den API-Schlüssel als Secret und binden ihn als Umgebungsvariable oder Datei unter /run/secrets/captchaai_key ein. Für Queue-Verarbeitung im großen Maßstab bieten sich Kubernetes-Jobs an.

Wie halte ich das Image möglichst klein?

Nutzen Sie den Multi-Stage-Build oben, --no-cache-dir bei pip install und eine schlanke requirements.txt. So landet nur der tatsächlich benötigte Code im finalen Image, und die Angriffsfläche bleibt minimal.


Verwandte Leitfäden


Bauen Sie Ihr erstes Solver-Image in wenigen Minuten – jetzt CaptchaAI-API-Schlüssel holen.

Kommentare sind für diesen Artikel deaktiviert.