DevOps والتوسع

Docker + CaptchaAI: حل اختبار CAPTCHA في حاوية

عندما ينتقل تكامل CaptchaAI من جهاز المطوّر إلى بيئة الإنتاج، تتحوّل "البيئة المتسقة" من رفاهية إلى شرط تشغيل: ما يعمل على حاسوبك المحلي قد يتعطّل على الخادم بسبب اختلاف إصدار Python أو مكتبة ناقصة. الحل المباشر هو تغليف سكربت الحل داخل حاوية Docker تحمل اعتمادياتها كاملة وتعمل بالطريقة نفسها في كل مكان — على جهازك، وعلى خادم الإنتاج، وداخل خط CI/CD.

يأخذك هذا الدليل عبر المسار كاملًا: من Dockerfile مكوّن من أسطر قليلة، مرورًا ببناء متعدد المراحل يقلّص حجم الصورة ويشغّلها بمستخدم غير جذري، وصولًا إلى أسطول من العمّال يتوسّع أفقيًا عبر Docker Compose وRedis. جميع الأمثلة تحل reCAPTCHA v2 عبر واجهة CaptchaAI، لكن النمط نفسه ينطبق على reCAPTCHA v3 وCloudflare Turnstile وبقية الأنواع المدعومة بمجرّد تغيير قيمة method.

لماذا تُشغّل حل الكابتشا داخل حاوية؟

تغليف الحل في حاوية يعالج ثلاث مشكلات تظهر عادةً مع أول محاولة للتوسّع:

  • بيئة موحّدة: إصدار Python والمكتبات مثبّتة داخل الصورة، فلا تتفاجأ بفروقات بين بيئة التطوير وبيئة الإنتاج.
  • توسّع أفقي بسيط: لزيادة الإنتاجية تُشغّل نسخًا إضافية من الحاوية بدل إعادة ضبط الخادم يدويًا.
  • نشر نظيف وقابل للتراجع: كل إصدار صورة موسوم، والعودة إلى إصدار سابق تتم بأمر واحد.

المتطلبات المسبقة

قبل البدء تحتاج إلى:

  • Docker مثبّتًا على جهازك أو خادمك (الإصدار 20.10 أو أحدث).
  • مفتاح CaptchaAI API صالحًا من لوحة التحكم في captchaai.com.
  • إلمامًا أساسيًا بلغة Python وسطر الأوامر.

نقطة البداية: Dockerfile بسيط

يبدأ كل شيء بصورة أساس خفيفة. المثال التالي يستخدم python:3.11-slim، وينسخ ملف الاعتماديات أولًا ثم الكود للاستفادة من طبقات الـ cache، والأهم أنه لا يخزّن مفتاح الـ API داخل الصورة بل يمرّره وقت التشغيل:

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 يكتفي بمكتبة واحدة لإرسال الطلبات إلى واجهة CaptchaAI:

requests>=2.31.0

سكربت الحل بلغة Python

سكربت الحل يتبع المنطق نفسه الذي تقوم عليه واجهة CaptchaAI: أرسل المهمة إلى in.php، احفظ معرّف المهمة، ثم استطلع res.php دوريًا حتى تعود الاستجابة بالتوكن. لاحظ أن المفتاح يُقرأ من متغيّر البيئة CAPTCHAAI_KEY لا من داخل الكود:

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

الحلقة تستطلع النتيجة كل خمس ثوانٍ لمدة تصل إلى دقيقتين، وتتعامل مع حالة CAPCHA_NOT_READY بالانتظار بدل رفع خطأ. نمط الاستطلاع الدوري هذا هو الأساس الذي سيُبنى عليه العامل متعدد المهام لاحقًا.

بناء الصورة وتشغيل الحاوية

بعد جاهزية الملفات، ابنِ الصورة وشغّلها ممرّرًا المفاتيح والقيم عبر متغيّرات البيئة — لا داخل أمر مخزَّن في سجلّ الأوامر لديك:

# 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

بناء متعدد المراحل لبيئة الإنتاج

صورة الإنتاج يجب أن تكون أصغر وأكثر تحصينًا. البناء متعدد المراحل يفصل مرحلة تجميع الاعتماديات عن مرحلة التشغيل، فلا تحمل الصورة النهائية أدوات البناء، ويعمل الحل بمستخدم غير جذري (solver) بدل root:

  • طبقة بناء منفصلة تُثبّت الاعتماديات في مجلد مستقل.
  • طبقة تشغيل نظيفة تنسخ الاعتماديات فقط دون أدوات التجميع.
  • مستخدم غير جذري يقلّل أثر أي ثغرة محتملة.
# 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

لرفع الإنتاجية تُشغّل عدة نسخ من الحاوية خلف قائمة انتظار مشتركة. ملف Docker Compose التالي يعرّف عامل حل، وخدمة Redis كقائمة انتظار، وعاملًا يسحب المهام منها، مع أربع نسخ (replicas) لكل خدمة عاملة:

# 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

حدود الذاكرة والمعالج لكل نسخة تمنع عاملًا واحدًا من استهلاك الخادم بالكامل، بينما restart: unless-stopped يضمن عودة العامل تلقائيًا بعد أي تعطّل.

مواءمة عدد العمّال مع خطة الـ thread

هنا نقطة يغفل عنها كثيرون: عدد الحاويات العاملة يجب أن يتماشى مع نموذج التسعير في CaptchaAI. فالخدمة تُحاسِب على أساس عدد الـ thread المتزامنة — أي عدد عمليات الحل الجارية في وقت واحد، مع حلول غير محدودة لكل thread طوال الشهر — لا على أساس كل عملية حل على حدة.

تخيّل فريق QA في متجر إلكتروني بالخليج يشغّل اختبارات آلية كل ليلة. على خطة BASIC — 15 دولارًا شهريًا مقابل 5 thread متزامنة — لا معنى لتشغيل 20 حاوية عاملة، لأن التنفيذ الفعلي يظل محدودًا بخمس عمليات حل متزامنة. وعند الانتقال إلى خطة ADVANCE — 90 دولارًا شهريًا مقابل 50 thread — تستطيع أربع حاويات أو أكثر أن تستغل الحصة كاملة. القاعدة العملية: اجعل إجمالي التزامن عبر حاوياتك قريبًا من عدد الـ thread في خطتك، لا أعلى منه.

العامل الذي يعتمد على قائمة انتظار Redis

العامل التالي يسحب المهام من قائمة Redis واحدة تلو الأخرى، يعالجها عبر واجهة CaptchaAI، ثم يخزّن النتيجة. هذا يفصل استقبال المهام عن تنفيذها، ويتيح زيادة عدد العمّال دون تغيير منطق الحل:

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

بما أن كل عامل يقرأ من القائمة نفسها، فإن إضافة نسخ جديدة توزّع الحِمل تلقائيًا دون أي تنسيق يدوي بينها.

إدارة مفاتيح الـ API ومتغيّرات البيئة بأمان

لا تضع مفتاح الـ API داخل الصورة أو في سجلّ الأوامر إطلاقًا. ضعه في ملف .env مستبعَد من Git، ومرّره إلى Docker Compose وقت التشغيل:

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

في بيئات الإنتاج الأكثر حساسية، استخدم أسرار Docker أو Kubernetes بدل ملف .env: تُثبَّت الأسرار كملفات وتُقرأ من /run/secrets/captchaai_key، فلا يظهر المفتاح ضمن متغيّرات البيئة ولا في سجلّات النظام.

حل المشكلات الشائعة

معظم أعطال التشغيل داخل الحاويات ترجع إلى مفتاح مفقود أو شبكة غير متاحة أو ذاكرة محدودة. يلخّص الجدول التالي أكثرها شيوعًا وطريقة معالجته:

المشكلة السبب المحتمل الحل
الحاوية تتوقف فور تشغيلها مفتاح CAPTCHAAI_KEY غير مُمرَّر مرّر -e CAPTCHAAI_KEY=... عند التشغيل
فشل استعلام DNS لا يوجد وصول للشبكة تحقّق من إعدادات شبكة Docker
استهلاك ذاكرة مرتفع عدد كبير من الطلبات المتزامنة حُدّ من ذاكرة الحاوية وعدد العمليات المتزامنة
المفتاح مكشوف داخل الصورة المفتاح مضمَّن في Dockerfile استخدم متغيّرات البيئة أو أسرار Docker

الأسئلة الشائعة

لماذا أُشغّل CaptchaAI داخل Docker بدلًا من تثبيته مباشرة على الخادم؟

لأن الحاوية تحمل بيئتها كاملة، فتختفي مشكلات "يعمل على جهازي فقط"، ويصبح النشر والتراجع والتوسّع الأفقي أسهل بكثير، خصوصًا عند تشغيل عدة عمّال في وقت واحد.

كيف أُبقي صورة الحاوية صغيرة وآمنة في الإنتاج؟

استخدم بناءً متعدد المراحل لاستبعاد أدوات التجميع من الصورة النهائية، واعتمد صورة أساس slim، وشغّل الحل بمستخدم غير جذري. هذه الخطوات الثلاث معًا تقلّص الحجم وتحدّ من أثر أي ثغرة.

كم عامل حل يجب أن أُشغّل؟

اربط العدد بخطتك: التزامن الفعلي محدود بعدد الـ thread في خطة CaptchaAI. ابدأ بأربعة عمّال، وراقب زمن الانتظار في القائمة، ثم زد العدد تدريجيًا حتى تقترب من حصة الـ thread المتاحة لك.

هل يعمل هذا الإعداد على Kubernetes أو Docker Swarm؟

نعم. النمط نفسه — صورة عديمة الحالة تقرأ المهام من قائمة انتظار — ينتقل مباشرة إلى Kubernetes أو Docker Swarm، مع الاستفادة من إدارة الأسرار والتوسّع التلقائي المدمجين فيهما.

مقالات ذات صلة


غلّف حلّك في حاوية واحدة وابدأ مع CaptchaAI اليوم.

التعليقات غير مفعّلة لهذا المقال.