Во вторник, в шесть вечера, я опубликовал в канал анонс новой статьи и пошёл проверить сервер. SSH не отвечал. Сайт не отвечал. Ping терял все пакеты. Я проверил с восьми точек мира через check-host: Москва, Германия, Швеция, США. Везде таймаут.

Сам я в тот момент сидел в интернете нормально, Хабр и Telegram открывались. Значит, умер не мой канал, а сервер. Чуть позже пришли новости: в ночь на 2 ноября хостинг Beget взломали, и данные облачных VPS уничтожены. Мой сайт, два бота и мониторщики к тому моменту лежали уже полтора дня, а реклама в Директе всё это время исправно водила людей в пустоту.

Это разбор того, что у меня пропало, что спасло и на какие грабли я наступил, пока поднимал всё заново. Отдельная часть про сюрприз нового сервера: с него не открылись ни Telegram API, ни API модели, и старый сервер, как выяснилось, работал просто по везению.

Что случилось у хостинга

По сообщению самого Beget, злоумышленники получили доступ к управляющему контуру инфраструктуры и запустили обнуляющие скрипты. В облаке (VPS, S3, Kubernetes) данные утрачены, бинарные бэкапы VPS тоже. Сохранились файловые копии, но клиентам их пока не выдают: каждый из более чем тысячи серверов проверяют вручную (новость на Хабре, Коммерсант, Ведомости).

Ждать файловую копию я не стал. Сроков никто не обещал, а сайт с рекламой простаивать не должен.

Что было на сервере и что пропало

На одном VPS (2 ядра, 4 ГБ) жили:

  • сайт студии и форма заявок;

  • бот-консультант в Telegram и в MAX (Python, aiogram, FastAPI, PostgreSQL, Redis, всё в Docker Compose, перед ними Caddy);

  • мониторщик заказов с бирж и мониторщик новостей для каналов;

  • приложение для цеха другого проекта и VPN-туннель третьего.

Что

Итог

Код бота, сайта, конфиги выкладки

цел: лежал у меня локально

Ключи и токены

целы, но засвечены: копия была в .env на сервере

База: заявки, пользователи, история мониторщиков

потеряна: копий вне сервера не было

DNS домена

у того же Beget, панель недоступна

С базой мне повезло в одном: живых заявок там почти не было, большинство тестовые. История мониторщика новостей пропала целиком, и вот это жалко, я собирался по ней писать.

Первым делом — ключи

Beget сам рекомендовал считать скомпрометированным всё, что лежало на серверах. Даже если данные никто не скачивал, проверить это я не могу. Поэтому до любой выкладки:

  • токен Telegram-бота — /revoke у @BotFather;

  • токен бота MAX — перевыпуск на платформе для партнёров;

  • ключ API модели — новый в консоли, старый удалить;

  • секреты вебхуков и пароль базы — новые, их всё равно генерирует скрипт.

Новые ключи я проверил до выкладки: getMe у Telegram и MAX, список моделей у API. Мелочь, но если вписать неработающий токен, потом долго ищешь, почему молчит бот.

Новый сервер за вечер, потому что выкладка была скриптом

Это то, что меня спасло. Ещё в октябре я сложил выкладку в один скрипт с шагами: подготовка сервера, генерация .env, сайт, бот, проверка. Тогда это казалось лишней формальностью. В аварию весь переезд свёлся к нескольким командам:

bash deploy.sh <ip> <домен> setup   # Docker, ufw (22/80/443), вход только по ключу
bash deploy.sh <ip> <домен> env     # .env: токены с моей машины по stdin, секреты генерируются на сервере
bash deploy.sh <ip> <домен> site
bash deploy.sh <ip> <домен> bot     # сборка, миграции на пустой базе, запуск
bash deploy.sh <ip> <домен> check   # страницы, healthz, закрытые служебные пути

Новый VPS я взял у другого провайдера, в Москве, на 8 ГБ: на нём снова будут жить все три проекта, а на старых 4 ГБ было впритык. SSH-ключ тоже новый: открытая часть старого лежала в authorized_keys на взломанной машине.

Миграции прошли на пустой базе с первой до восьмой, контейнеры поднялись. Осталось направить домен.

DNS, когда панель регистратора лежит

Домен зарегистрирован в том же Beget, его DNS тоже там. Основная панель не открывалась. Выручила отдельная страница управления DNS, которую Beget поднял как раз для таких случаев (новость): вводишь домен, проходишь капчу, получаешь код на почту администратора домена и правишь A-записи.

У записи стоял TTL 600 секунд, и публичные резолверы Google и Яндекса быстро начали отдавать новый адрес. А сайт не открылся.

Причина нашлась в логе Caddy. Он попросил сертификат у Let’s Encrypt сразу при старте, когда домен ещё смотрел на мёртвый адрес. Проверка, естественно, не прошла, и Caddy ушёл в повтор с растущей паузой. После перезапуска контейнера сертификаты выпустились за минуту. Вывод на будущее: сначала DNS, потом запуск веб-сервера, или хотя бы перезапуск после смены записей.

Тестовая заявка с сайта дошла карточкой в рабочий чат MAX, я закрыл её кнопкой. Сайт, форма и MAX-бот снова работали. Telegram и модель — нет.

Сюрприз: с нового сервера не видно Telegram и модели

Вот что показал curl с нового сервера:

api.anthropic.com   403  {"type":"forbidden","message":"Request not allowed"}
api.telegram.org    000  (таймаут 20 с)
platform-api2.max.ru  отвечает
habr.com              отвечает

API модели отказывает этой сети по географии, Telegram Bot API не отвечает вовсе. На старом сервере оба работали, и я принимал это как должное. Видимо, у старого адреса была другая репутация. Строить продакшен на везении конкретного IP было ошибкой, просто раньше она не проявлялась.

Сначала я попробовал готовые конфиги коммерческого VPN, которым пользуюсь на рабочем компьютере. На сервере обычный WireGuard не завёлся: к прямому узлу рукопожатие не проходило вовсе, через их российский узел проходило, но полезный трафик не шёл. Похоже, без их приложения и его варианта протокола эти конфиги с российского сервера бесполезны.

В итоге я сделал проще и надёжнее: арендовал самый маленький VPS в Нидерландах (1 ядро, 1 ГБ, 613 ₽ в месяц) и вынес на него только исходящие запросы бота к Telegram и к модели. Сайт, MAX, база и всё остальное работают напрямую, как раньше.

Было → стало: один сервер против московского и маленького зарубежного
Было → стало: один сервер против московского и маленького зарубежного

Со стороны бота это одна настройка и две строки. Прокси задаётся переменной окружения, и если её нет, бот ходит напрямую:

# config.py
outbound_proxy: str | None = None   # http://host:port — для Telegram и модели; MAX ходит напрямую
# клиент модели: прокси только для него, остальные httpx-клиенты не трогаем
from anthropic import AsyncAnthropic, DefaultAsyncHttpxClient

client = AsyncAnthropic(
    api_key=settings.anthropic_api_key, timeout=60, max_retries=1,
    http_client=DefaultAsyncHttpxClient(proxy=settings.outbound_proxy) if settings.outbound_proxy else None,
)
# aiogram: своя сессия с прокси (нужен пакет aiohttp-socks)
from aiogram.client.session.aiohttp import AiohttpSession

bot = Bot(
    token=settings.bot_token,
    default=DefaultBotProperties(parse_mode=ParseMode.HTML),
    session=AiohttpSession(proxy=settings.outbound_proxy) if settings.outbound_proxy else None,
)

Можно было выставить HTTPS_PROXY на весь контейнер, SDK модели его подхватывает сам. Я не стал: тогда через Нидерланды пошли бы и запросы мониторщиков к российским сайтам, а им зарубежный адрес ни к чему.

Telegram без вебхука

После этого бот достучался до Telegram, setWebhook прошёл. А сообщения не приходили. getWebhookInfo всё объяснил:

pending_update_count: 7
last_error_message:   Connection timed out

Исходящие запросы теперь шли, но входящие от серверов Telegram до российского адреса не доходили. Семь сообщений висели в очереди у Telegram.

Решение — забирать обновления самому, long polling через тот же прокси. MAX при этом остаётся на вебхуке, у него с входящими всё в порядке. Раньше режим был общий на оба мессенджера, поэтому я разделил его:

# config.py
bot_mode: Literal["webhook", "polling"] = "webhook"
telegram_mode: Literal["webhook", "polling"] | None = None   # только Telegram; MAX остаётся на вебхуке

@property
def telegram_transport(self) -> str:
    return self.telegram_mode or self.bot_mode
# main.py, запуск Telegram
if settings.telegram_transport == "polling":
    # pending updates сохраняем: те семь сообщений должны дойти
    await bot.delete_webhook(drop_pending_updates=False, request_timeout=15)
    polling_task = asyncio.create_task(
        dp.start_polling(bot, allowed_updates=allowed, handle_signals=False),
        name="telegram-polling",
    )

Одна деталь при переключении: delete_webhook(drop_pending_updates=True), который часто пишут в примерах для локального запуска, выбросил бы накопившиеся сообщения. Я оставил их, и бот разобрал очередь сразу после старта. Ответ на одно сообщение так и не ушёл: этот пользователь к тому времени бота заблокировал.

Ещё одно решение из октября здесь пригодилось. Запуск Telegram у меня обёрнут так, что его падение не роняет форму на сайте и MAX: бот пишет в лог «Telegram недоступен» и пробует снова раз в несколько минут. Поэтому, пока я разбирался с Telegram, сайт и MAX спокойно работали.

Сколько стоили три с половиной дня

Самая обидная часть — реклама. Кампания в Директе прошла модерацию как раз 2 ноября и начала показываться в день аварии. Сайт её не волновал.

Клики по рекламе и визиты на сайт по дням
Клики по рекламе и визиты на сайт по дням

День

Расход

Клики

Визиты в Метрике

2 ноября

315 ₽

17

0

3 ноября

604 ₽

33

0

4 ноября

647 ₽

38

0

5 ноября

423 ₽

29

13 (сайт поднялся вечером)

6 ноября

717 ₽

36

40

Около 1 900 ₽ из 2 700 ушли на клики, которые упирались в неработающий сайт. О паузе я подумал ещё во вторник, но отложил: рассчитывал переехать быстро. Переезд занял двое суток.

Зато 6 ноября, в первый полный день работы, пришла первая живая заявка с рекламы. Так что сама связка работает, просто ей нужен живой сервер.

Что бы я сделал иначе

  • Бэкап базы вне сервера, каждую ночь. У меня его не было, и это главная потеря. Теперь это первая задача: pg_dump по расписанию на другую машину.

  • Код в git с удалённым репозиторием, а не только в синхронизируемой папке. Мне повезло, что локальная копия была полной.

  • DNS у другого провайдера, не у того, где сервер. Если хостинг лежит целиком, домен должен оставаться в моих руках. Мне помогла аварийная страница Beget, но рассчитывать на такое нельзя.

  • Внешний мониторинг с уведомлением. Я узнал о падении через полтора дня, и то случайно. Проверка /healthz с другого сервера раз в пять минут сберегла бы и время, и деньги на рекламе.

  • Пауза рекламы сразу, как только сайт не отвечает. Вручную, но в тот же час.

  • Проверять при выборе хостинга, открываются ли с него нужные API, и держать прокси для внешних запросов в одной настройке. При переезде тогда меняется одна строка.

Сейчас сайт, форма, оба бота и мониторщики снова работают. Ночной бэкап базы — следующее, что я делаю.

Комментарии (8)


  1. Pude
    07.11.2026 08:39

    Именно поэтому я и выбирал Beget, так как у них Telegram работал идеально из России. А теперь нужно искать сервер в РФ, чтобы там тоже работал Telegram


    1. Aflubin1978 Автор
      07.11.2026 08:39

      У меня то же самое: на старом сервере Telegram Bot API отвечал, а с нового московского не отвечает вовсе. Похоже, дело было в репутации конкретного адреса, а не в хостинге. Я в итоге вынес исходящие запросы на маленький зарубежный сервер, всё остальное живёт в Москве. Схема — в разделе про сюрприз нового сервера.


  1. mosinnik
    07.11.2026 08:39

    вот прям сам хостер взял и стер виртуалку?


    1. Aflubin1978 Автор
      07.11.2026 08:39

      Нет, конечно, сам хостер ничего не удалял, заголовок вышел неточным, поправил. По сообщению Beget, злоумышленники получили доступ к управляющим серверам и запустили скрипты, которые стирали данные. В облаке пропали и сами VPS, и их бинарные бэкапы. Ссылки на сообщения — в начале статьи.


  1. radhab20
    07.11.2026 08:39

    Бегету сложно будет отправиться после этого случая.


    1. fanatikvoice
      07.11.2026 08:39

      Очень сложно будет. Вся эта история напоминает ihor хостингом - репутация подмочена значительно, и теперь им веры мало.


    1. etonormalno
      07.11.2026 08:39

      банкротство и новое имя только поможет


  1. etonormalno
    07.11.2026 08:39

    прикол что мучился с выбором VPS и в итоге продлил бигет. Тогда он нагнулся. И теперь опять возник вопрос, какой хостер по цене и качеству выбрать? !