DevOps y Escalado

Docker + CaptchaAI: resolución de CAPTCHA en contenedores

"En mi máquina funciona" deja de ser una excusa cuando empaquetas el solver en una imagen Docker: código, dependencias y API key entran de forma controlada y la misma imagen corre igual en tu portátil, en un VPS o en un clúster. Esta guía va del Dockerfile mínimo al despliegue con Docker Compose y varios workers en paralelo. Es el patrón que usa una agencia de scraping o QA en Madrid o Ciudad de México para resolver miles de CAPTCHA al día con un costo mensual en USD predecible.


Requisitos previos

Antes de construir la imagen conviene tener a mano tres cosas:

  • Docker y Docker Compose instalados en tu máquina o servidor.
  • Tu API key de CaptchaAI, que obtienes desde el panel de control.
  • El site_key y la URL de la página cuyo CAPTCHA quieres resolver.

El Dockerfile base: imagen mínima y clave fuera de la imagen

Parte de python:3.11-slim y copia solo lo necesario. Regla de oro: la API key nunca se hornea en la imagen; se inyecta como variable de entorno al arrancar.

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

El script del solver: reCAPTCHA v2 con la API

El solver envía la tarea al endpoint in.php y sondea res.php cada 5 segundos hasta recibir el token. El ejemplo usa userrecaptcha, pero la misma estructura sirve para cualquier tipo compatible: reCAPTCHA v2/v3, Cloudflare Turnstile y Challenge, GeeTest v3, image/OCR y grid.

# 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]}...")

Como el solver habla con la API por HTTP, el contenedor no necesita Chrome ni navegador headless: la imagen queda pequeña.


Construye la imagen y ejecútala

docker build genera la imagen y docker run la lanza con sus variables de entorno; --rm limpia el contenedor al terminar.

# 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

Build multi-stage para producción: sin root y más ligera

En producción, separa la compilación de la ejecución. El patrón multi-stage te da tres ventajas:

  • Instalas dependencias en un builder y copias solo el resultado a la imagen final.
  • Corres el proceso como usuario no privilegiado, sin root.
  • Obtienes una imagen más ligera que arranca más rápido.
# 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"]

Docker Compose: escalar workers en paralelo

Aquí rinde la contenerización. El archivo define tres servicios que trabajan juntos:

  • solver-worker: 4 réplicas del solver, con límites de memoria y CPU por contenedor.
  • redis: la cola compartida entre todos los workers.
  • queue-worker: consume tareas de Redis y escribe los resultados.

Con replicas levantas varios workers idénticos. Cada uno procesa entre 5 y 10 resoluciones simultáneas: 4 réplicas cubren de 20 a 40 tareas en paralelo.

# 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

Detalle clave: tu concurrencia real no la fija el número de contenedores, sino tus threads. CaptchaAI cobra por thread concurrente con resoluciones ilimitadas, sin coste por CAPTCHA ni cargos por tipo. Para 40 tareas simultáneas, STANDARD ($30/mes, 15 threads) se queda corto y ADVANCE ($90/mes, 50 threads) las cubre; puedes empezar con BASIC ($15/mes, 5 threads) y escalar después.


Worker con cola de Redis

El worker se bloquea en blpop, resuelve cada CAPTCHA contra la API y guarda el resultado en un hash de Redis. El patrón desacopla al productor del solver: añades o quitas workers sin tocar quien encola.

# 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()

Variables de entorno y secretos

Guarda la API key en un .env que nunca subes a Git; con --scale ajustas los workers en caliente.

# .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

Diagnóstico de problemas comunes

Problema Causa Solución
El contenedor sale inmediatamente Falta CAPTCHAAI_KEY Pasa -e CAPTCHAAI_KEY=...
La resolución DNS falla Sin acceso a la red Verifica la configuración de red de Docker
Alto uso de memoria Demasiadas solicitudes simultáneas Limita la memoria y la concurrencia del contenedor
Clave API expuesta en la imagen Clave en el Dockerfile Usa variables de entorno o Docker secrets

Preguntas frecuentes

¿Cuántos threads necesito para el número de contenedores que ejecuto?

Depende de tu concurrencia objetivo, no del número de contenedores. Calcula el pico de tareas en paralelo y elige el plan que lo cubra: 4 workers con 5-10 resoluciones cada uno piden un plan tipo ADVANCE ($90/mes, 50 threads).

¿Cómo evito filtrar la API key en la imagen?

Nunca la escribas en el Dockerfile. Pásala como variable de entorno o móntala como Docker secret; así viaja fuera de la imagen y no queda en el historial de capas.

¿Necesito Chrome o un navegador headless dentro del contenedor?

No. El solver habla con la API por HTTP, así que basta con Python y requests. Sin navegador, la imagen es más pequeña y consume menos memoria por réplica.

¿Puedo usar Docker secrets en lugar de un archivo .env?

Sí. Docker Swarm y Kubernetes admiten secretos: móntalos como archivos y léelos desde /run/secrets/captchaai_key. Es lo recomendado en producción frente a un .env en disco.

¿Qué tipos de CAPTCHA puedo resolver desde el contenedor?

Los que soporta la API: reCAPTCHA v2/v3, Cloudflare Turnstile y Challenge, GeeTest v3, image/OCR y grid. Solo cambias el method y los parámetros; la lógica del contenedor no varía.


Guías relacionadas

Los comentarios están deshabilitados para este artículo.