"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_keyy 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
buildery 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.