Решатель 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 сегодня.