Если совсем коротко, то чтобы поднять проект после трёх месяцев простоя, мало просто залить старый бэкап на новый сервер и нажать «старт», и я это знал, но не до конца понимал насколько. Я брал отдельную машину на Timeweb (4 ядра, 8 гигов памяти, 80 гигов диска за 1800 рублей в месяц), переносил туда сразу оба окружения, восстанавливал базу из весеннего бэкапа и проигрывал поверх неё 49 миграций. И самое интересное вылезло не в инфраструктуре, а в двух мелочах, которые я бы сам прохлопал: один эндпоинт падал бы пятисоткой на свежем проде, потому что часть данных сидится не миграцией, а ручным скриптом, а обещание «все ваши стрики сохранены» оказалось бы прямой ложью, если бы я не вписал одну строку SQL в ночь рестора. Вот про это и расскажу по порядку.
Сервер был неоплачен, проект лежал
Картара — это мой Telegram Mini App с AI-персонажем, и где-то на три месяца он просто умер. Не от бага, не от наплыва юзеров, а банально потому, что я отвлёкся на другие проекты и перестал платить за сервер. «Сервер ожил. был неоплачен» — это дословно из моих заметок того дня, и честнее не скажешь. То есть данные живы, код живой, но всё это лежало мёртвым грузом, пока в какой-то момент я не решил, что пора возвращать.
Возвращать я решил не на старое место, а на отдельную машину, и вот почему. Раньше Картара делила инфраструктуру с другим моим проектом, и это всё время мешало — то одно зацепишь, то другое, и каждый раз приходилось держать в голове, что эти двое сидят рядом. Так что я взял свежий сервер на Timeweb, Ubuntu 24.04, те самые 4 ядра, 8 гигов памяти и 80 гигов диска за 1800 рублей в месяц. Сервер российский, и это не от патриотизма, а потому что embeddings и Whisper от OpenAI у нас геоблочены, так что приходится держать сервер в РФ плюс прокси для запросов наружу. И на эту машину переезжали оба окружения сразу, и dev, и prod, чтобы старый проект наконец полностью освободился от Картары и они перестали дышать друг другу в затылок.
Мартовский бэкап и 49 миграций поверх
Дальше начинается та часть, которую все недооценивают. У меня есть локальный бэкап базы с весны — это снимок того состояния, в котором проект был жив в последний раз. Но код-то с тех пор уехал вперёд, за это время накопились миграции, и схема базы в коде уже не совпадает со схемой в этом дампе. То есть нельзя просто восстановить дамп и запустить новый код, он будет работать со старой структурой и развалится на первом же месте, где ждёт нового поля.
Значит, план такой: восстановить базу из мартовского бэкапа, а потом проиграть поверх неё миграции с 056 по 104. Это 49 файлов. Я специально проверил, что пара номеров в середине пропущена, сначала напрягся, нет ли тут дыры, но нет, это нормально, такие гэпы остаются, когда миграцию по ходу удаляли или переименовывали. То есть эти 49 миграций должны последовательно догнать мартовскую схему до того состояния, в котором живёт текущий код. Звучит просто, и обычно так оно и проходит, но именно тут я заставил себя не торопиться и прогнать всё сначала на пустой базе, и только потом трогать реальные данные.
И сразу честно: я тут не сам в терминале сидел и SQL руками набивал. Код и весь этот деплой-пайплайн под моим управлением писал агент, а я оркестрировал, я код руками вообще не пишу и никогда не умел. Но именно потому, что выполняет всё агент, а не человек, который интуитивно чувствует неладное, мне важно было прогнать отдельный аудит готовности к деплою. И вот этот аудит нашёл то, что иначе ушло бы в прод незамеченным.
Эндпоинт, который упал бы у первого же пользователя
Первое, что вылезло, я для себя пометил как «деплой-бомбу». Суть в том, что служебные позиции для раскладов (их там девять штук) на старом проде давным-давно засеяли вручную, отдельным скриптом, который кто-то когда-то запустил руками и забыл. В миграциях этого сида нет. То есть на старой машине эти данные просто лежат в базе, и всё работает, а на свежем проде, который я поднимаю с нуля, их нет — миграции их не создают.
И что в итоге получается? Юзер заходит, жмёт «получить результат», дёргается эндпоинт генерации, лезет за этими позициями, не находит их и отдаёт честную пятисотку. То есть фича, которая на старом сервере годами работала, на новом упала бы сразу, у первого же пользователя, в самый ответственный момент — когда человек только вернулся и решил проверить, что проект вообще жив. Лучше способа убить доверие на старте сложно придумать. Сам фикс примитивный, добавить этот сид в нормальную миграцию, чтобы он накатывался автоматом, а не жил в голове у человека, который его когда-то руками прогнал. Но вся работа была не в фиксе, а в том, чтобы найти эту мину до взрыва.
Стрики: я бы соврал людям, сам того не зная
А вот второй косяк мне нравится даже больше, потому что он не про код, а про то, что я чуть не пообещал людям то, чего не будет. План на возвращение был такой: заливаем бэкап, утром делаем рассылку «мы вернулись», пишем пост в канал, и в голове у меня крутилось примерно так, мол зальём бэкап базы и у людей стрики будут стоять как стояли. То есть я был уверен, что человек, который был активен весной, вернётся и увидит свою серию заходов нетронутой.
А теперь как это работает на самом деле. Сброс стрика в Картаре жёстко календарный, безо всякой грации и заморозки. Логика простая: если последний заход был не вчера и не сегодня, серия обнуляется в единицу. И вот представьте: человек был активен в середине марта, накопил, скажем, сорок дней подряд, возвращается в июне за подарком, видит свой стрик 40, радуется, жмёт кнопку — и в этот самый момент система сравнивает дату его последнего входа с сегодняшней, видит трёхмесячную пропасть и обнуляет всё в единицу. Он своими же руками только что обнулил то, чем гордился. Сорок превратилось в один, и виноват как будто он сам.
Это та самая ситуация, где технически всё работает по спецификации, а по-человечески ты обманул людей. И обиднее всего, что в посте я бы честно написал «все ваши стрики сохранены», искренне в это веря, а оно бы сломалось у каждого на первом же тапе.
Фикс на одну строку
Решение оказалось маленьким до смешного. В ночь рестора, до того как юзеры вернутся, прогнать одну строку, которая всем разом проставит дату последнего входа на «вчера» — что-то вроде «UPDATE ... SET последний_вход = вчерашняя дата». И всё. Тогда первый июньский заход система засчитает не как обрыв серии, а как нормальное продолжение, вчера заходил, сегодня зашёл, стрик жив, плюсуется дальше. Одна строка SQL, прогнанная в правильный момент и в правильном порядке, спасает обещание, которое иначе было бы враньём.
Я думал, делать ли вообще полноценную систему грации, заморозку стриков, дни-отгулы, всю эту красоту. И решил, что нет, не сейчас. Как я сам себе сказал тогда: да не, это норм, кто хочет, тот вернётся и сохранит стрик, остальные сами решают, что и как им делать. Амнистия одной строкой закрывает конкретную боль конкретного релиза, а городить механику на будущее, которую может никто и не попросит, — это ровно то, чего делать не надо. Если YAGNI где-то и работает, то вот ровно здесь.
Что я из этого вынес
Порядок в итоге выстроился такой: сначала тест всего пайплайна на пустой базе, потом рестор реальной базы после полуночи, потом утренняя рассылка «мы вернулись» и только следом пост в канал. И ключевое тут — именно последовательность. Стрик-амнистию надо прогнать до того, как вернутся люди, сид раскладов — до первого вызова генерации, миграции — до запуска нового кода. Перепутаешь порядок, и любой из этих шагов превращается в ту самую пятисотку у первого же юзера.
Так что если вы похожим образом поднимаете проект после простоя или просто переезжаете на новый сервер с восстановлением прода, не относитесь к бэкапу как к кнопке «было как раньше». Между старым дампом и сегодняшним кодом всегда лежит расстояние: миграции, ручные сиды, которые никто не задокументировал, и логика вроде календарного стрика, которая на живом сервере годами работала молча и сломается ровно в момент рестора. И самое дорогое тут вовсе не код, который пишет нейросеть под моим управлением, а вот это умение заранее спросить себя, а что именно сломается, когда вернутся реальные люди. Аудит до деплоя поймал две мины, каждая из которых ударила бы по доверию в первую же минуту. Вот и делайте выводы — лишний прогон на пустой базе и пара честных вопросов к самому себе стоят сильно дешевле, чем извинения перед людьми, которым ты пообещал то, чего система не умеет.