ESPNBills, Josh Allen outlast Rams in overtime on MNFESPN DeportesLaporta fija la Champions como el objetivo de Barcelona este añoInquirerFree dance sessions for breast cancer survivors set in CalapanThe Jerusalem PostHezbollah explosive smuggling, illegal finance network leads to calls for probe by Lebanese PMsRTP DesportoPlay-off Mundial2027. Portugal - Irlanda do Norte (19h00)Daily MaverickSERIES SETBACK: Proteas point to ‘soft dismissals’ after batting collapse hands Australia first TestInquirer EntertainmentWowie de Guzman figures in Batangas car accident, assures he’s OKBollywood HungamaMrunal Thakur neither confirms nor denies Raaka casting rumours, says film announcements will come on social mediaTaipei TimesENGLISH DIGEST 實用美語(空中美語)
The Daily Newsstand · Free, Always
Tuesday, October 13, 2026

Эвакуация за один день: как я перенёс два приложения и детектив-ARG из облака под ударами на собственный VPS

Translate

Привет, Хабр. Это продолжение истории студии из одного человека. В прошлый раз я рассказывал, как собрал детектив-ARG на чистой статике, а бэкенд моих двух приложений жил на serverless в Яндекс Облаке: Cloud Functions, YDB, Object Storage. Инфраструктура стоила ноль рублей в месяц, и я этим гордился.

В ночь на 8 октября беспилотники атаковали дата-центр Яндекса в Сасово. 9 октября - дата-центр в Калуге. В ночь на 11 октября - третий, во Владимире, после чего Yandex Cloud перешёл в аварийный режим: две зоны доступности недоступны, «оставшуюся конфигурацию ресурсов считаем неустойчивой». Пострадавших среди людей нет, и это главное. А у меня на этом облаке жили бэкенды двух опубликованных приложений и сайт с игрой.

12 октября я провёл собственную эвакуацию: за один день перенёс 40 роутов и 39 таблиц на один VPS, а утром 13-го у меня уже были алерты в телеграме, своя метрика посещаемости без сторонних скриптов и античит, которого не было никогда. Приложения в поле не заметили ничего.

Под катом - хронология, почему serverless эвакуировать труднее всех, и пять уроков, каждый из которых стоил мне пары часов.

<cut>

Неделя, когда горели дата-центры

Короткая хроника по открытым источникам:

  • Ночь на 8.10 - ЦОД в Сасово (Рязанская область): попадание, пожар, работа площадки полностью остановлена, зона ru-central1-b недоступна. Сбои у десятков компаний: Циан, девелоперы, автобусные билеты.

  • 9.10 - ЦОД в Калужской области: несколько модулей полностью выведены из строя.

  • Ночь на 11.10 - ЦОД во Владимире: работа остановлена, Yandex Cloud признан работающим в аварийном режиме, недоступны зоны ru-central1-a и ru-central1-b, более 80 сервисов компании деградировали.

Яндекс добавил атаки БПЛА в список форс-мажоров, договорился с Selectel, VK Cloud и К2Cloud об ускоренном размещении клиентов и рекомендовал задействовать планы восстановления на внешних площадках. Восстановление повреждённых мощностей, по оценкам экспертов, займёт от недель до месяцев.

Почему serverless эвакуировать труднее всех

Партнёрская программа эвакуации - отличная вещь, если у тебя виртуальные машины и кластеры Kubernetes: их есть куда переносить и на чём поднять. Но моя инфраструктура была serverless: функции, документная БД YDB с AWS-совместимым API, бакеты. Это проприетарные API конкретного облака. «Ускоренное размещение у партнёров» для функции не работает - её нельзя поднять на чужой площадке, её можно только переписать.

Мои активы на тот момент: бэкенд Копилки Отказов (25 роутов, 24 таблицы), бэкенд Судоку Арены (15 роутов, 15 таблиц), статика сайта с тремя сезонами ARG, телеграм-бот поддержки. Нагрузка маленькая, данные почти пустые - это и было преимуществом: тащить почти нечего, переносить надо код.

Спасло то, что код изначально был на обычном стеке: Node 22 + TypeScript + Hono.js. Никакой облачной экзотики внутри логики - только адаптеры к YDB по краям. Значит, переносим, а не переписываем. Цель: один VPS, docker compose, фиксированный чек в месяц.

Урок нулевой: проверяй виртуализацию до оплаты

Эвакуация началась с ложного старта. Первый VPS, который я поднял утром 12 октября, выглядел рабочим: система ставится, docker ставится, деплой проходит. Но:

$ swapon /swapfile
swapon: /swapfile: swapon failed: Operation not permitted

Swap невозможен. Дальше: ufw роняет сеть контейнера (в ядре нет модуля conntrack), fail2ban не банит (нет iptables-модулей), dockerd стартует только с iptables: false и host-сетью. Поддержка честно ответила: модули включать не будем. Диагноз - OpenVZ, контейнерная виртуализация с общим ядром хоста. Прод с биллингом на площадке без swap и файрвола я не держу.

Проверка занимает секунду:

$ systemd-detect-virt
openvz    # ← стоп, уходим
kvm       # ← работаем

Теперь это шаг ноль любого захода на новый сервер. Второй провайдер прошёл проверку (KVM), и дальше всё заняло один день.

День эвакуации

temp-infra/
├── Caddyfile           # три домена, статика сайта + прокси на api
├── docker-compose.yml  # caddy / api / games / cron
├── services/
│   ├── api/            # Копилка: 25 роутов + 2 фоновые задачи, 24 таблицы
│   ├── games/          # Судоку: 15 роутов + 2 крона, 15 таблиц
│   ├── cron/           # push-tick поминутно, кампании почасово, лига вс 23:59
│   └── bot/            # телеграм-боты (живут на втором VPS, об этом ниже)
└── site/               # статика сайта и ARG

Порядок: инвентарь всех роутов функций (таблица «роут - что делает - какие таблицы»), затем порт один-в-один - те же пути, те же форматы ответов, та же JWT-логика. YDB заменена на better-sqlite3 с теми же схемами. Данные не эвакуировали: их было почти ноль, честнее начать с чистой БД - подписки и лига наполнятся новыми событиями.

Ключевое решение: домены не менялись. A-записи api.tempapp.ru и api-games.tempapp.ru переключились на новый IP, TLS перевыпустился через Let’s Encrypt, и приложения в поле продолжили ходить по тем же адресам на новый бэкенд. Никакого «обновите приложение из стора».

Деплой - 20 строк bash: rsync репозитория, docker compose up -d --build, обратная связь ~50 секунд. Вечером того же дня на боевых доменах был зелёный смоук: авторизация с ротацией токенов, идемпотентность покупок, лимиты, рефералка end-to-end.

Параллельно с переездом на сайт выкатились второй и третий сезоны детектива - сборка уехала тем же rsync’ом, что и весь сайт. Эвакуация инфраструктуры и релиз контента случились в одни сутки, и это лучший знак: процесс не останавливался ни на час.

Урок первый: оффсайт-копия - это не опция

Сасово доказало то, о чём все читают в учебниках: пожар в дата-центре - не метафора из презентации про DR, а твой прод. Поэтому схема бэкапов строилась сразу под «сгорел ЦОД»:

  • дамп SQLite каждый час, десять последних локально;

  • раз в сутки дамп уезжает rsync’ом на второй VPS - тот самый, где уже живут мои тг-боты;

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

Восстановление «с нуля»: купить VPS - ssh-copy-id - ./deploy.sh - поднять SQLite из последнего дампа. Цель - полчаса от «всё сгорело» до «работает». На этой неделе это перестало быть паранойей.

Урок второй: docker игнорирует ufw

Публикуешь порт в compose:

ports:
  - "3001:3001"

- и docker вставляет правило в цепочку DOCKER-USER до правил ufw. ufw status честно показывает, что 3001 закрыт, а он доступен снаружи. Мой телеграм-релей должен принимать запросы только с основного сервера, и «закрыто в ufw» было иллюзией.

Лечение - правило именно в DOCKER-USER:

iptables -I DOCKER-USER -p tcp --dport 3001 ! -s <IP_ОСНОВНОГО_СЕРВЕРА> -j DROP

плюс systemd-юнит, который поднимает его после перезагрузки (docker сбрасывает цепочку при рестарте). Проверка: с чужого адреса - таймаут, со своего - 200.

Урок третий: fetch не считает 403 ошибкой

Баг, который жил сутки и нашёлся случайно. Алерты устроены так: кроны шлют POST /alert на телеграм-релей с заголовком x-internal-secret. В коде было:

await fetch(process.env.TG_RELAY, { method: 'POST', body: JSON.stringify({ text }) });
// ну отправили и отправили

fetch резолвится при любом HTTP-статусе. 403 - не исключение. А заголовок в одном из вызовов забыли проставить. Итог: сутки алерты молча умирали на релее - система уведомлений была слепа к собственной поломке.

Теперь канон:

const res = await fetch(url, opts);
if (!res.ok) throw new Error(`alert relay: HTTP ${res.status}`);

Мета-вывод: у системы уведомлений должен быть хотя бы один путь, не зависящий от неё самой. У меня это утренний дайджест: если ночного дампа нет - дайджест скажет «ДАМПА НЕТ», а идёт он по уже проверенному маршруту.

Урок четвёртый: compose restart не перечитывает .env

Мелочь, стабильно кусающая всех: docker compose restart перезапускает контейнер со старым окружением. Отредактировал .env, сделал restart - переменная в контейнере старая. Правильно:

docker compose up -d   # перечитывает env_file и пересоздаёт контейнер при изменениях

Бонус раз: метрика без Метрики

Яндекс.Метрика на сайте перестала работать, чинить сторонний скрипт в эти дни было странным приоритетом, а вопрос «заходит ли вообще кто-нибудь» остался. Сайт - статика за Caddy, значит всё уже пишется в access-лог. Включил JSON-логи и написал скрипт в cron-контейнер: каждое утро в 09:00 он парсит вчерашние сутки и шлёт мне в телеграм дайджест - уникальные пары IP+User-Agent (погрешность на NAT принята осознанно), топ-10 страниц, топ-5 источников, сколько отсеяно ботов. Плюс строки из SQLite: регистрации за сутки, активные подписки, состояние дампа и диска.

Ноль скриптов на клиенте, ноль персональных данных, блокировщикам нечего блокировать. Минус: событийной воронки внутри страниц нет - верну, когда будет что мерить.

Бонус два: античит для вечнозелёной игры

ARG на статике имеет конфузную особенность: все ответы лежат в JS - открой консоль, и вот они. Пока игроков было мало, это была почти фича («копающий свой» - часть жанра). На фоне переезда прибрался и тут.

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

const digest = await crypto.subtle.digest('SHA-256',
  new TextEncoder().encode(`temp:${season}:${word.toLowerCase().trim()}`));

Соль не даёт сравнить хэш с радугой, нормализация (trim, lower, ё→е) не наказывает за опечатки. Перебор словаря по-прежнему возможен - и это нормально: барьер поднят с «открой консоль» до «напиши скрипт», а кто написал - тот свой. Платный четвёртый сезон будет проверяться сервером (там деньги, требования другие), архив из трёх сезонов останется офлайн навсегда.

Что в итоге

Один день - и инфраструктура перестала быть «облаком, где-то там». Теперь это конкретная машина с известным чеком: дампы каждый час, ночная копия на втором сервере, утром дайджест, при первой оплате - мгновенный алерт. Два телеграм-бота (поддержка и админский) живут на том самом втором VPS - Telegram API из российских сетей недоступен, - и он же принимает ночные бэкапы.

Что бы я сделал иначе: начал бы с systemd-detect-virt - сэкономил бы вечер. И имел бы план эвакуации до того, как он понадобился: весь вторник я импровизировал, и только мелкость моей инфраструктуры сделала импровизацию успешной.

Ссылки: сайт с детективом - https://tempapp.ru (второй и третий сезоны выкатились прямо на фоне переезда, «Инвентаризация» - уже на новом сервере. Копилка Отказов и Судоку Арена - в RuStore. Телеграм-канал студии: https://t.me/tempapp_news

Спрашивайте в комментариях - отвечу на всё техническое. На сюжетное: без спойлеров, дело открыто. Командам Яндекса и Selectel/VK Cloud/К2Cloud, которые эвакуируют клиентов в эти дни, - уважение: неделя у них тяжелее моей.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.