15 KiB
Деплой hh_auto на физический Linux-сервер (многопользовательский, без VNC)
Архитектура
Браузер пользователя
│ HTTPS (443)
┌───────┴───────┐
│ nginx │ Let's Encrypt
│ reverse proxy│
└───────┬───────┘
│ :5000
▼
gunicorn (Flask: авторизация, изоляция данных, лимиты генераций)
│
▼
Playwright (Chromium: headless или на Xvfb :99)
│
▼
скриншот заполненной формы → проверка в веб-интерфейсе → кнопка «Отправить»
Ключевые принципы:
- Авторизация — логин/пароль на входе в веб-интерфейс (Flask-сессия)
- Изоляция данных — у каждого пользователя своя папка
users/<логин>/с его аккаунтами hh.ru, резюме, cookies, прогрессом - Без VNC — браузер работает на сервере (headless или на виртуальном экране Xvfb), заполненная форма показывается скриншотом в веб-интерфейсе, человек подтверждает отправку кнопкой
- Лимиты — дневной лимит генераций на пользователя (вы платите за LLM)
- LLM-ключ общий (в
.envна сервере) — пользователи его не видят
Как работает отклик без VNC:
- Пользователь вставляет cookies hh.ru (из своего браузера) — они подставляются в браузер на сервере автоматически
- Playwright открывает вакансию, жмёт «Откликнуться», выбирает резюме, вставляет текст
- Делает скриншот заполненной формы → показывает его в веб-интерфейсе
- Человек смотрит скриншот и жмёт «Отправить отклик» → скрипт кликает кнопку в браузере
- Вакансия помечается отправленной
Модель пользователей и данных
/opt/hh_auto/
├── .env # LLM_API_KEY, PROXY_URL, SESSION_SECRET, HH_HEADLESS (chmod 600)
├── users.json # пользователи: логин, hash пароля, лимит, счётчик генераций
├── manage_users.py # CLI: add / list / passwd / limit / remove
└── users/
├── admin/ # вы (владелец)
│ └── accounts/
│ └── my_hh/ # config.json, resume.txt, session.txt,
│ # profile/, progress.json, screenshots/
└── friend1/ # друг
└── accounts/
└── friend1_hh/
users.json:
{
"admin": {"password_hash": "...", "llm_limit": 50, "llm_usage": {"2026-09-12": 3}, "created": "2026-09-12"},
"friend1": {"password_hash": "...", "llm_limit": 20, "llm_usage": {}, "created": "2026-09-12"}
}
Пароли — только хэши (werkzeug), в открытом виде нигде не хранятся.
Предположения
- ОС: Debian 12 / Ubuntu 22.04+, есть root
- Домен или статический IP; порты 22, 80, 443 открыты
- Круг пользователей небольшой (2–10 человек). Два варианта:
- Открытая регистрация (по умолчанию): любой может создать аккаунт на странице
/register - Закрытая (
ALLOW_REGISTRATION=0в.env): пользователей создаёт владелец черезmanage_users.py
- Открытая регистрация (по умолчанию): любой может создать аккаунт на странице
Этап 0 — подготовка сервера
apt update && apt upgrade -y
useradd -m -s /bin/bash hh
apt install -y ufw
ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable
Этап 1 — системные пакеты
apt install -y python3 python3-venv python3-pip \
nginx certbot python3-certbot-nginx \
xvfb fonts-liberation
# Зависимости Chromium (если playwright install-deps недоступен):
apt install -y libnss3 libxcomposite1 libxdamage1 libxrandr2 libgbm1 \
libasound2 libatk-bridge2.0-0 libcups2 libpango-1.0-0 libcairo2 \
libxkbcommon0 libatspi2.0-0
Xvfb нужен только если браузер работает «видимым» (рекомендуется — меньше шансов, что hh.ru распознает автоматизацию). Если хотите совсем без дисплея — поставьте
HH_HEADLESS=1в.env, тогда Xvfb не нужен.
Этап 2 — деплой кода
cd /opt && git clone https://git.itguild.info/apuc/hh_auto
chown -R hh:hh /opt/hh_auto
su - hh
cd /opt/hh_auto
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/playwright install chromium
cat > /opt/hh_auto/.env << 'EOF'
LLM_API_KEY=ваш_ключ_здесь
LLM_BASE_URL=https://opencode.ai/zen/v1
LLM_MODEL=deepseek-v4-flash
PROXY_URL=http://LOGIN:PASSWORD@185.168.251.163:8000
SESSION_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
HH_HEADLESS=0
HH_NO_SANDBOX=1
EOF
chmod 600 /opt/hh_auto/.env
mkdir -p /opt/hh_auto/users && chmod 700 /opt/hh_auto/users
HH_NO_SANDBOX=1— если Chromium не стартует из-за sandbox (root/контейнер).PROXY_URL— если hh.ru блокирует IP сервера.
Этап 3 — виртуальный дисплей (если HH_HEADLESS=0)
cat > /etc/systemd/system/xvfb.service << 'EOF'
[Unit]
Description=Xvfb virtual display :99
After=network.target
[Service]
ExecStart=/usr/bin/Xvfb :99 -screen 0 1400x900x24 -nolisten tcp +extension GLX
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now xvfb
Этап 4 — пользователи
Вариант А — открытая регистрация (по умолчанию, ALLOW_REGISTRATION=1):
любой посетитель создаёт аккаунт сам на странице /register (логин + пароль,
сразу входит). Лимит генераций для таких пользователей — REGISTER_LLM_LIMIT
(по умолчанию 50 в день). Владельца всё равно стоит создать через CLI:
# Владелец (лимит 50 генераций в день)
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py add admin --password СЛОЖНЫЙ_ПАРОЛЬ --limit 50
Вариант Б — закрытая регистрация (ALLOW_REGISTRATION=0 в .env):
пользователей создаёт только владелец:
# Владелец (лимит 50 генераций в день)
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py add admin --password СЛОЖНЫЙ_ПАРОЛЬ --limit 50
# Друг (лимит 20)
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py add friend1 --password ПАРОЛЬ_ДРУГА --limit 20
# Просмотр
/opt/hh_auto/.venv/bin/python /opt/hh_auto/manage_users.py list
Другие команды: passwd <логин> (смена пароля), limit <логин> <N> (лимит, 0 = без лимита), remove <логин> (удаление с данными).
Этап 5 — веб-приложение
5.1 Gunicorn
cat > /etc/systemd/system/hh-web.service << 'EOF'
[Unit]
Description=hh_auto Flask app (gunicorn)
After=network.target xvfb.service
[Service]
User=hh
Group=hh
WorkingDirectory=/opt/hh_auto
Environment=DISPLAY=:99
EnvironmentFile=/opt/hh_auto/.env
ExecStart=/opt/hh_auto/.venv/bin/gunicorn \
-w 1 --threads 8 --timeout 300 -b 127.0.0.1:5000 webui:app
Restart=always
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now hh-web
-w 1: один воркер — in-memory кэш и браузеры живут в процессе.--timeout 300: открытие вакансии в браузере занимает до минуты.DISPLAY=:99: браузер открывается на Xvfb (если HH_HEADLESS=0).
5.2 Nginx + HTTPS
# Сертификат (домен):
certbot certonly --webroot -w /var/www/html -d hh.example.com
# Или самоподписанный (IP):
openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/hh.key \
-out /etc/ssl/certs/hh.crt -days 365 -nodes -subj "/CN=hh-auto"
cat > /etc/nginx/sites-available/hh-auto << 'EOF'
server {
listen 80;
server_name hh.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name hh.example.com;
ssl_certificate /etc/letsencrypt/live/hh.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hh.example.com/privkey.pem;
# Если самоподписанный:
# ssl_certificate /etc/ssl/certs/hh.crt;
# ssl_certificate_key /etc/ssl/private/hh.key;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600;
}
}
EOF
ln -sf /etc/nginx/sites-available/hh-auto /etc/nginx/sites-enabled/hh-auto
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
systemctl enable --now certbot.timer
Этап 6 — первый вход и проверка
- Откройте
https://hh.example.com/→ страница входа → логинadmin - Создайте аккаунт hh.ru (имя, запросы, регион), загрузите резюме
- Войдите на hh.ru в своём браузере → F12 → Application → Cookies → скопируйте все cookies (
hhtoken,hhuid,hhrole,_xsrf) → вставьте в поле «Ключ сессии hh.ru» → «Сохранить ключ» - «Обновить список» рядом с «Резюме на hh.ru» → выберите резюме
- «Найти вакансии» → отметьте нужные → «Сгенерировать тексты» → проверьте/отредактируйте
- «Открыть в браузере» → появится скриншот заполненной формы → проверьте → «Отправить отклик»
- То же самое друг делает под своим логином — видит только свои аккаунты и свой лимит
Этап 7 — бэкапы и мониторинг
# /opt/hh_auto/backup.sh — бэкапим users.json + users/ + .env
#!/bin/bash
BACKUP_DIR=/var/backups/hh_auto
mkdir -p "$BACKUP_DIR"
DATE=$(date +%Y%m%d_%H%M)
tar czf "$BACKUP_DIR/hh_auto_$DATE.tar.gz" \
-C /opt/hh_auto users.json users .env
find "$BACKUP_DIR" -name '*.tar.gz' -mtime +30 -delete
echo "0 3 * * * root /opt/hh_auto/backup.sh" > /etc/cron.d/hh-backup
chmod 644 /etc/cron.d/hh-backup
# Логи
journalctl -u hh-web -f
# Здоровье
curl -sf http://127.0.0.1:5000/api/accounts > /dev/null || systemctl restart hh-web
Чек-лист проверки
| Тест | Ожидаемый результат |
|---|---|
https://DOMAIN/ без логина |
Редирект на страницу входа |
| Вход admin / friend1 | Каждый видит только свои аккаунты |
Попытка friend1 открыть /api/accounts/admin |
404 |
| Поиск вакансий | Список вакансий (или капча — см. «Риски») |
| Генерация текстов | Текст отклика; счётчик в шапке растёт |
| Превышение лимита | «Достигнут дневной лимит генераций» |
| «Открыть в браузере» | Скриншот заполненной формы |
| «Отправить отклик» | Кнопка нажата, вакансия помечена |
journalctl -u hh-web |
Без ошибок |
Риски и обходы
hh.ru блокирует IP дата-центра / капча
- Симптом: капча при поиске, HTTP 403, пустой список
- Обход:
PROXY_URLв.env; если не помогает — residential-прокси
hh.ru распознаёт автоматизацию
- Симптом: капча именно при открытии вакансии в браузере
- Обход:
HH_HEADLESS=0+ Xvfb (видимый Chromium, сложнее распознать); свежие cookies; не слишком частые действия
Cookies инвалидируются
- Симптом: поиск пуст, скриншот показывает страницу входа
- Обход: заново скопировать cookies из браузера
Chromium не стартует
- Симптом:
hh-webпадает, вjournalctlошибки - Обход:
HH_NO_SANDBOX=1; проверитьDISPLAY=:99иsystemctl status xvfb; проверить зависимости (ldd ~/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome | grep "not found")
Одновременные пользователи
- Один gunicorn-воркер и один браузерный поток — операции выполняются по очереди. Для 2–10 пользователей достаточно; при большей нагрузке — переработка архитектуры
Расходы на LLM
- Ключ общий, генерации пользователей идут за ваш счёт. Лимиты задаются в
manage_users.py limit <логин> <N>
Миграция с однопользовательской версии
- Старые
accounts/*→users/admin/accounts/*(простоmv)
Итого
| Этап | Время |
|---|---|
| Подготовка сервера + пакеты | 15–20 мин |
| Деплой кода + .env | 5 мин |
| Xvfb (опционально) | 5 мин |
| Пользователи (manage_users) | 5 мин |
| Gunicorn + nginx + HTTPS | 15–20 мин |
| Первый вход + проверка | 15–20 мин |
| Итого | ~1–1.5 часа |
Что получают пользователи
- Свой логин/пароль → свой веб-интерфейс (только свои аккаунты, резюме, вакансии)
- Скриншоты заполненных форм → сами подтверждают отправку
- Свой дневной лимит генераций
- Не видят: чужие аккаунты, резюме, cookies, прогресс, LLM-ключ, пароли