DevOps & Scaling

Docker + CaptchaAI: контейнерное решение CAPTCHA

Решатель CaptchaAI разворачивается в Docker как обычный сетевой сервис: образ, ключ переменной окружения и нужное число реплик. Ниже — путь от одного контейнера до очереди воркеров на Docker Compose и типичные грабли контейнеризации.

Контейнеризация решает конкретную DevOps-задачу: воркер решения CAPTCHA, который живёт «руками» на одном сервере, рано или поздно упирается в невоспроизводимое окружение — разные версии requests, забытые переменные, ручной перезапуск после падения. Docker убирает этот класс проблем: образ собирается один раз и разворачивается одинаково на ноутбуке, в CI и на проде, а масштабирование сводится к числу реплик.


Базовый Dockerfile для решателя

Минимальный образ на python:3.11-slim: ключ передаётся при запуске, а не зашивается в слои образа. Слой с зависимостями кэшируется отдельно от кода, поэтому пересборка после правки solver.py занимает секунды, а не минуты.

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

Что даёт такой минимальный образ на практике:

  • база slim вместо полного python:3.11 — меньше поверхность для уязвимостей и быстрее docker pull на CI-раннере;
  • единственная зависимость requests — не тянет за собой лишние системные пакеты и компиляторы;
  • ключ через ENV CAPTCHAAI_KEY="" без значения по умолчанию — контейнер не может случайно стартовать с чужим или тестовым ключом.

Скрипт решателя

solver.py отправляет задачу в in.php и опрашивает res.php до готовности токена — стандартный цикл submit/poll CaptchaAI. Опрос идёт с фиксированным интервалом в 5 секунд и жёстким потолком в 24 попытки, поэтому воркер не зависает бесконечно на одной сложной задаче.

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

Логика намеренно простая: одна функция без внешнего фреймворка задач, всего два HTTP-вызова (in.php и res.php) и понятное исключение при таймауте. Для одиночного контейнера этого достаточно; очередь на несколько воркеров понадобится дальше, когда нагрузка вырастет.


Сборка и запуск контейнера

Ключ передаётся через -e, а не через ARG/ENV внутри Dockerfile — так он не осядет в слоях образа. docker history покажет значения ARG/ENV, заданные при сборке, поэтому ключ, зашитый в Dockerfile, лежит в открытом виде даже после удаления строки из исходника.

# 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

SITE_KEY и PAGE_URL в примере — плейсхолдеры для локальной проверки, в проде их подставляет оркестратор задач или значения приходят из очереди (см. раздел про воркер ниже).


Многоэтапная сборка для продакшена

Двухэтапный Dockerfile даёт компактный образ и запуск не от 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"]

Что даёт двухэтапная сборка по сравнению с базовым вариантом:

  • инструменты сборки и кэш pip остаются в промежуточном слое builder и не попадают в конечный образ;
  • процесс работает под пользователем solver, а не под root — при компрометации контейнера у процесса нет прав на хост;
  • PYTHONUNBUFFERED=1 заставляет вывод сразу идти в stdout, поэтому docker logs показывает сообщения в реальном времени.

Docker Compose: несколько воркеров решения CAPTCHA

Один контейнер редко закрывает реальную нагрузку. Ниже — конфигурация на 4 реплики плюс отдельная очередь на Redis: solver-worker держит прямые вызовы к API, queue-worker разбирает задачи из Redis, а сам Redis выступает буфером между источником задач и воркерами.

# 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

Для стека на европейском VPS (например, во франкфуртском регионе Hetzner — частый выбор команд из СНГ) лимиты memory/cpus в deploy подгоняйте под реальную нагрузку, а не копируйте из примера.

На что смотреть при выборе числа реплик:

  • сколько задач в среднем стоит в очереди в часы пик — реплики должны разбирать её быстрее, чем она растёт;
  • лимит memory: 256M рассчитан на один процесс с requests; при добавлении библиотек пересчитайте лимит заранее, а не по факту OOM-килла;
  • Redis без volume теряет очередь при перезапуске — для прода подключите persistent-том или вынесите Redis за пределы compose-стека.

Обработчик задач из очереди

Воркер забирает задачу из очереди captcha:tasks, решает тем же циклом submit/poll и кладёт результат в captcha:results. От прямого вызова API его отличает то, что источник задачи и получатель результата разнесены во времени — воркер может упасть и перезапуститься, не потеряв уже поставленные в очередь задачи.

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

r.blpop блокирует воркер до появления новой задачи в списке captcha:tasks, поэтому процесс не крутится вхолостую и не нагружает CPU опросом пустой очереди. Результат кладётся в хэш captcha:results под task_id — вызывающая сторона забирает его отдельным запросом, когда ей удобно, а не ждёт синхронного ответа от воркера.


Переменные окружения и хранение секретов

Ключ CaptchaAI не должен попадать в Git — держите его в .env и исключайте файл из репозитория. Один и тот же принцип работает и для локальной разработки, и для CI: секрет живёт вне репозитория и подставляется средой выполнения.

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

Ротацию ключа при такой схеме сделать проще: замените значение в .env (или в секрете оркестратора), перезапустите контейнеры — и старый ключ нигде в образе не остаётся.


Логи и наблюдаемость воркеров

Контейнеры пишут в stdout/stderr, а не в файл — это стандартное поведение для Docker, и queue_worker.py в примере выше следует ему намеренно (print вместо файлового логгера). Смотреть логи одного воркера: docker logs -f <container>; для всего стека Compose — docker compose logs -f queue-worker.

Что стоит вынести на панель мониторинга, если задач много:

  • длину очереди captcha:tasks в Redis — растущая очередь сигнализирует, что реплик не хватает;
  • долю ответов с {"error": ...} в captcha:results — рост доли ошибок обычно означает проблему с ключом, балансом или недоступностью in.php/res.php;
  • время между постановкой задачи и появлением результата — это ваш реальный SLA для потребителя, а не только время ответа API.

Типичные проблемы и их решение

Проблема Причина Решение
Контейнер сразу завершает работу Не задан CAPTCHAAI_KEY Передайте -e CAPTCHAAI_KEY=... при запуске
Не разрешается DNS Нет доступа к сети Проверьте настройки сети Docker
Высокое потребление памяти Слишком много параллельных запросов Ограничьте память контейнера и число одновременных задач
API-ключ виден в образе Ключ прописан прямо в Dockerfile Передавайте ключ через переменные окружения или секреты
Воркер запущен, но задачи не решаются Неверный ключ или закончился баланс потоков Проверьте баланс и статус ключа в панели управления CaptchaAI

Часто задаваемые вопросы

Как передать API-ключ CaptchaAI в контейнер, не запекая его в образ?

Только переменной окружения при запуске (-e CAPTCHAAI_KEY=...) или секретами Docker/Kubernetes. Ключ в Dockerfile остаётся в истории слоёв образа даже после удаления файла из репозитория — docker history покажет его любому, у кого есть доступ к образу.

Slim или Alpine — какой базовый образ Python лучше для решателя?

python:3.11-slim практичнее для этого сценария: requests и redis ставятся без сборки нативных пакетов. Alpine компактнее по размеру, но musl вместо glibc часто удлиняет сборку зависимостей — выигрыш в мегабайтах редко стоит потерянного времени CI.

Как передать CAPTCHAAI_KEY через Docker secrets или Kubernetes Secrets?

Swarm и Kubernetes монтируют секреты как файлы в контейнере. Читайте значение из /run/secrets/captchaai_key — так ключ не попадёт в docker inspect, переменные окружения процесса и логи оркестратора.

Почему контейнер завершается сразу после docker run?

Чаще всего отсутствует CAPTCHAAI_KEY — скрипт проверяет переменную на старте и выходит с кодом 1. Проверьте docker logs <container> сразу после падения: сообщение об ошибке в solver.py явно называет отсутствующую переменную.

Нужен ли для прода отдельный managed-инстанс Redis, или хватит контейнера из compose?

Контейнер redis из примера подходит для разработки и небольшой нагрузки. Для прода, где очередь нельзя терять при перезапуске, вынесите Redis за пределы compose-стека — на отдельный инстанс с persistence.


Похожие руководства


Контейнеризируйте свой решатель — получить CaptchaAI сегодня.

Комментарии для этой статьи отключены.