Репозиторий — это только половина приложения. Вторая половина живёт в панели платформы, в настройках соседних сервисов, в DNS-зоне и в привычках самой платформы. Проще всего увидеть её, если не читать список услуг, а пройти за одним действием пользователя. Возьмём условный интернет-магазин, и пусть это будет нажатие кнопки «Оформить заказ». За секунду до ответа «Спасибо, заказ принят» код успевает обратиться к базе, поставить задачу в очередь, отправить письмо и сохранить квитанцию. Каждый из этих шагов опирается на что-то, чего нет в архиве с исходниками.
Дальше мы идём по этому одному пути и на каждом шаге задаём два вопроса: кто владеет данными и как этот шаг вызывается. Ответ на первый вопрос подскажет, что нужно переносить. Ответ на второй покажет, что нужно заново запускать. Условный магазин здесь учебный: его устройство придумано, чтобы показать приёмы, а ваше приложение может быть устроено иначе.
Одно нажатие, пять следов
Пусть магазин устроен обычно: веб-процесс принимает форму, пишет заказ в реляционную базу, кладёт задачу «подготовить квитанцию и письмо» в очередь, а отдельный рабочий процесс забирает её, формирует PDF и отправляет клиенту письмо со ссылкой. После оплаты платёжный провайдер присылает на адрес магазина уведомление, и статус заказа меняется. По ночам что-то по расписанию закрывает неоплаченные заказы.
Если выписать всё это в таблицу, видно, что в архив с кодом попадает только первый столбец.
| Шаг | Владелец данных | Как вызывается | Что выяснить до переезда |
|---|---|---|---|
| Запись заказа | База данных (управляемая или своя) | Строка подключения из окружения | Где физически живёт база, какая версия, какие расширения |
| Задача в очередь | Брокер очереди | Клиентская библиотека, адрес из окружения | Кто хранит невыполненные задачи и что с ними станет в момент переключения |
| Рабочий процесс | Платформа запускает его по описанию | Отдельный тип процесса или отдельная служба | Где записано, что он вообще существует и сколько его копий |
| Письмо | Почтовый сервис плюс DNS-записи домена отправителя | API или SMTP с ключом | Какой домен подтверждён и кто управляет его зоной |
| Квитанция | Выясняется отдельно, см. следующий раздел | Запись файла кодом | Куда файл попадает на самом деле |
| Уведомление об оплате | Платёжный провайдер хранит адрес обратного вызова | Входящий HTTP-запрос от провайдера | Какой URL записан в личном кабинете провайдера |
| Ночное закрытие заказов | Расписание в панели платформы или внутри приложения | Таймер, которого может не быть в репозитории | Кто запускает задачу и что случится, если запустят две копии |
Обратите внимание на колонку «Как вызывается». Ни один из этих способов не является свойством вашего кода. Строка подключения приходит из окружения, рабочий процесс запускается платформой, расписание срабатывает снаружи, а уведомление об оплате приходит из чужого сервиса на адрес, записанный в чужом кабинете. Переезд ломается именно на таких связях: код при этом может быть безупречен.
Квитанция: три слоя вместо одного вопроса
Начнём с самого коварного шага. В коде рабочего процесса есть строка, которая сохраняет квитанцию. Допустим, она записывает PDF в каталог вроде ./tmp/receipts/ относительно рабочей папки приложения. На самом деле у такой записи три слоя, и каждый нужно проверить отдельно.
- Код. Какой путь формируется, из чего он складывается (номер заказа, дата, случайный суффикс) и что происходит после записи: сохраняется ли путь в базе, отправляется ли файл дальше, строится ли ссылка для письма.
- Локальный путь записи. В какую файловую систему этот путь попадает на старой платформе.
- Реальное хранилище. Где файл лежит после того, как процесс завершился, и откуда клиент получает его по ссылке из письма.
Эти слои легко перепутать, потому что в коде они выглядят как одно действие «сохранить файл». Но между строкой кода и местом, где файл переживает остановку, может стоять что угодно: переменная окружения с именем каталога, обёртка над объектным хранилищем, сетевой диск, который платформа монтирует по описанию.
Что говорят о локальной файловой системе сами платформы
Документация платформ описывает локальные диски осторожно, и различия между ними существенны. В контракте среды выполнения Cloud Run сказано, что локальная файловая система контейнера находится в памяти: записанные данные расходуют память и теряются, когда экземпляр останавливается (Cloud Run: Container runtime contract). Там же отмечено, что services по умолчанию масштабируются: число экземпляров меняется под запросы, события и нагрузку процессора.
В документации DigitalOcean App Platform про хранение данных речь о другом, но вывод похож. Локальная файловая система экземпляра эфемерна и пропадает после повторного развёртывания или замены контейнера, а у разных экземпляров приложения файловые системы отдельные. Для постоянных данных там называют объектное хранилище и управляемую базу, а тома на дату проверки платформа не поддерживает (DigitalOcean App Platform: Store data).
Отсюда не следует, что у вашего магазина квитанции пропадали. Это тот самый случай, когда неизвестное поведение нельзя подменять догадкой. Возможны как минимум три картины, и они ведут к разным действиям при переезде.
| Что обнаружилось | Кто владеет файлом | Что это значит для VPS |
|---|---|---|
| Код пишет в локальный каталог, и больше ничего с файлом не происходит | Экземпляр приложения на старой платформе | Архива для переноса нет: решите, нужны ли старые квитанции, и откуда клиенты получали ссылки. Новую запись нужно будет положить на диск VPS или в объектное хранилище осознанно |
| Код пишет локально, затем загружает файл в бакет по ключу из окружения | Объектное хранилище, а локальный файл — временная копия | Переносить нужно бакет и ключи доступа (или оставить бакет на месте и сохранить доступ к нему), а не каталог |
| Код пишет в каталог, который платформа монтирует из внешнего диска или сетевой файловой системы | Внешнее хранилище, описанное вне репозитория | Нужно найти описание монтирования и воспроизвести его: каталог на диске VPS, сетевая точка монтирования или смена на объектное хранилище |
Какая из строк таблицы ваша, показывает не чтение документации, а наблюдение. Один раз проведите тестовый заказ на старой платформе, найдите в логах рабочего процесса путь, под которым записан файл, и проверьте три вещи. Во-первых, существует ли этот путь внутри запущенного экземпляра, если зайти в него через консоль платформы (если она предоставляет такой доступ). Во-вторых, появился ли объект с таким именем в бакете или другом хранилище, которое указано в окружении. В-третьих, что именно лежит в письме: ссылка на ваш домен, подписанный временный адрес или вложение.
Последний пункт часто решает всё. Если ссылка в письме ведёт на публичный адрес бакета, то клиент скачивает квитанцию не с вашего веб-процесса, и значит, старые письма продолжат работать, пока бакет жив. Если ссылка ведёт на маршрут приложения, который читает файл с диска, то после переезда он станет искать файл уже на новом диске. Одинаковый код в этих двух мирах ведёт себя по-разному, потому что разный владелец у данных.
Что меняется, когда квитанции оказываются на диске VPS
На VPS с root-доступом каталог на NVMe-диске переживает перезапуск процесса и перезагрузку машины, пока вы сами не удалите файлы. Это снимает вопрос эфемерности, но приносит новые вопросы, которые раньше решала платформа. Нужно задать каталог переменной окружения, а не зашивать в код. Нужно назначить владельца каталога: пользователь, от имени которого работает приложение, должен писать в него, а веб-сервер — читать. Нужно прикинуть, как быстро растёт каталог: если квитанция — это PDF в сотни килобайт, а заказов несколько тысяч в месяц, то это сотни мегабайт, и диск в 40 ГБ на тарифе Ba заполнится не скоро. Но этот расчёт зависит от ваших реальных цифр, и подставить их нужно самостоятельно. Наконец, нужно решить, кто копирует каталог на случай потери машины: автоматических резервных копий во всех тарифах рассчитывать не стоит, значит, копирование — часть вашего плана запуска.
Если же на старой платформе уже стоит объектное хранилище, переезд на VPS не обязан его менять. Приложение может продолжать писать в тот же бакет по тем же ключам, а переносить придётся только вычислительную часть. Это самый спокойный вариант, но он оставляет у старого провайдера зависимость, о которой нужно помнить при расчёте затрат и сроков отключения аккаунта.
Секреты и конфигурация, которых нет в репозитории
Теперь вернёмся к строкам подключения. Приложение берёт их из окружения, и это правильно: секреты не должны лежать в репозитории. Но именно поэтому они не попадут в архив с кодом, и список переменных в панели платформы становится вторым источником правды о приложении.
Соблазн понятен: открыть панель, выгрузить все переменные и перенести их в файл на сервере. Такой список полезен, но он не полон по двум причинам. Во-первых, платформа может подставлять собственные переменные, которых вы не создавали: порт, на котором нужно слушать, имя экземпляра, служебные адреса. Их значения на VPS придётся выбирать самим. Во-вторых, некоторые настройки вообще не живут в окружении: токен в конфигурационном файле, который создаётся на этапе сборки, секрет подписи уведомлений, вписанный в код провайдера, сертификат, подкладываемый платформой как файл.
Надёжнее встретить список с двух сторон. Первый заход: найти в коде и в сценариях сборки все обращения к окружению и выписать только имена.
grep -rhoE 'process\.env\.[A-Z0-9_]+' src | sort -u
grep -rhoE 'os\.environ(\.get)?\(?.[A-Z0-9_]+' app | sort -uВторой заход: выписать имена из панели платформы, не копируя значений. Теперь сравните два списка. Имя есть в коде, но нет в панели — значение зашито по умолчанию либо приложение падает без него, и это надо проверить. Имя есть в панели, но нет в коде — переменная либо лишняя, либо читается библиотекой косвенно, либо используется на этапе сборки. Каждое расхождение — вопрос, который стоит задать до переезда, а не после. Сам по себе такой разбор не доказывает, что вы нашли всё: библиотеки читают окружение сами, и некоторые настройки формируются из строк в рантайме. Поэтому последняя проверка — не чтение, а запуск.
Для условного магазина у каждой переменной свой владелец. Строка подключения к базе принадлежит тому, кто базу хозяйничает: у провайдера или у вас. Ключ почтового API принадлежит почтовому сервису, и его можно перевыпустить, не трогая код. Секрет подписи уведомлений об оплате принадлежит платёжному провайдеру, и менять его нужно в двух местах сразу. Ключ доступа к бакету — владельцу хранилища. Когда вы рядом с каждым именем пишете, у кого лежит значение и как его заменить, вы заодно получаете план ротации, которая понадобится, если секреты окажутся в чужих логах или в пересланной переписке.
На VPS эти значения удобно хранить в файле, который читает служба: например, как EnvironmentFile для юнита systemd или как env-файл для контейнера. Права на файл ограничьте так, чтобы читать его мог только пользователь приложения и root. Реальные значения не должны попадать ни в репозиторий, ни в тикеты, ни в сообщения чата при согласовании переезда: достаточно имён и признака «задано/не задано».
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Расписания и фоновые процессы, которые запускала платформа
Самое неприятное в фоновых задачах то, что они не выдают себя. Веб-страница либо открывается, либо нет. А если ночью перестало закрываться неоплаченное и не вышла рассылка напоминаний, об этом узнают через несколько дней.
Платформы устраивают запуск по-разному, и это видно даже по трём примерам. У Cloud Run жизненный цикл services и jobs различается: service отвечает на запросы и может сократить число экземпляров, когда трафика нет (вплоть до настроенного минимума), а job выполняет задачу и завершается (Cloud Run: Container runtime contract). Если ваш рабочий процесс сидел внутри service и обрабатывал очередь, то на такой платформе это нужно проверять отдельно: он жив только пока жив экземпляр. В Netlify функции исполняют обработчик в управляемой среде по событию или запросу, а их код собирается, версионируется и развёртывается вместе с сайтом (Netlify: Functions overview). Значит, функция не работает как долгоживущий процесс, и её вызывает не ваш код, а платформа по своему событию.
Из этого следует практичный приём: для каждой фоновой работы запишите два свойства — что её вызывает и сколько живёт один запуск. Для магазина получается примерно такой реестр.
- Обработчик очереди (квитанции и письма). Вызывает: сообщение в очереди. Живёт долго, должен быть запущен постоянно. На VPS это отдельная служба с автоматическим перезапуском.
- Закрытие неоплаченных заказов. Вызывает: расписание. Живёт секунды. На VPS это systemd timer или запись в cron, а заодно решение, что делать, если предыдущий запуск ещё не закончился.
- Обработчик уведомлений об оплате. Вызывает: входящий HTTP-запрос. Живёт столько, сколько запрос. Это часть веб-процесса, но адрес и проверка подписи должны сохраниться.
Где искать то, что не описано в репозитории? Откройте раздел панели платформы с расписаниями и отдельными компонентами и сравните его с описанием процессов в проекте. Просмотрите историю запусков и логи за период, равный самому длинному интервалу в расписании: задача раз в месяц не покажется в логах за неделю. Загляните и внутрь кода: бывает, что расписание живёт в самом приложении через библиотеку-планировщик, и тогда при двух копиях приложения оно сработает дважды.
Последнее касается не только переезда, но и репетиции. Когда вы поднимаете копию магазина на VPS, у неё те же задачи, те же ключи и та же база-источник, если вы не развели их. Копия, которая рассылает напоминания реальным клиентам или закрывает настоящие заказы, — неприятная репетиция. Поэтому в тестовой копии расписания либо выключены, либо направлены на тестовые данные и тестовые почтовые адреса. Как организовать такую копию целиком, разобрано в материале про настройку staging-копии сайта.
Внешние обратные вызовы и имена доменов
Дальше путь заказа выходит за пределы вашего кода. Платёжный провайдер хранит у себя адрес, на который шлёт уведомление об оплате. Почтовый сервис хранит подтверждённый домен отправителя. Авторизация через внешнего провайдера хранит разрешённые адреса возврата. Всё это записано не у вас и не в репозитории, и все эти записи указывают на то имя, которое сейчас привязано к старой платформе.
Если переезд делается сменой DNS-записи, а имя домена остаётся прежним, большинство таких адресов продолжит работать само: запрос придёт на то же имя, только на другой сервер. Но здесь есть два условия. Первое: адрес в кабинете провайдера может быть не вашим доменом, а техническим адресом платформы вида «имя-приложения.платформа». Тогда после переезда провайдер продолжит слать уведомления на старое место, а новое окружение их не получит. Второе: перед сменой записи полезно заранее снизить срок кэширования (TTL), чтобы откат не растянулся на часы. Это стоит сделать за время, превышающее прежний TTL, иначе старое значение ещё будет жить в чужих кэшах.
Сделайте по каждому внешнему сервису короткую запись.
- Платёжный провайдер. Владелец адреса — провайдер, меняется в его кабинете. Вызов — входящий запрос. Что проверить: какой именно адрес записан, как провайдер подписывает запрос и откуда берётся секрет проверки, что провайдер делает, если ответ не получен (повторяет ли попытки и как долго — это смотрят в его документации).
- Почтовый сервис. Владелец доменных записей — тот, кто управляет DNS-зоной. Вызов — исходящий запрос с ключом. Что проверить: какие записи нужны для подтверждения домена отправителя и входят ли в них адреса, привязанные к старой платформе. Если письма раньше уходили через почтовый API по HTTPS, то смена сервера на них не влияет; если через SMTP, то нужно убедиться, что исходящее соединение с нужным портом с нового сервера разрешено.
- Сервисы с ограничением по IP. Если внешний API разрешал запросы только с известных адресов, новый адрес сервера нужно добавить в список. Не рассчитывайте, что чужой API по умолчанию примет запросы с любого нового адреса: это решает владелец API, и проверять нужно заранее и тестовым запросом.
Отдельная история — имена внутри самого приложения. Часто в настройках встречается собственный публичный адрес магазина: он нужен для ссылок в письмах и для возврата с платёжной страницы. Если он сохранился с техническим адресом старой платформы, письма после переезда будут вести клиентов туда, где приложения уже нет. Простейшая проверка — тестовый заказ с письмом на тестовый ящик и внимательное чтение всех ссылок в нём.
Карта будущего запуска из найденных связей
К этому моменту у вас есть разрозненные записи: таблица пути заказа, три слоя квитанции, расхождения в именах переменных, реестр фоновых задач, список внешних адресов. Соберите из них одну карту, где у каждой строки заполнены пять полей: что это, кто владеет данными, как вызывается, где оно будет работать после переезда и как это проверить. Такая карта — не отчёт, а рабочий документ: по ней вы будете разворачивать окружение.
| Связь | Владелец данных | Способ вызова | Место после переезда | Как проверить |
|---|---|---|---|---|
| Заказы | База данных | Строка подключения в окружении | Своя база на VPS или прежняя управляемая | Тестовый заказ создаёт строку и читается обратно |
| Очередь задач | Брокер очереди | Клиентская библиотека | Служба на VPS | Сообщение из заказа попадает к обработчику |
| Обработчик очереди | Описание процессов (вне кода) | Постоянный процесс | Служба с автоперезапуском | Остановить и убедиться, что поднимется и продолжит |
| Квитанция | Определяется тестом на старой платформе | Запись файла + выдача ссылки | Каталог на диске или объектное хранилище | Файл открывается по ссылке из письма после перезапуска службы |
| Письмо | Почтовый сервис, DNS-зона | Исходящий API/SMTP | Тот же сервис, проверенные записи | Письмо доходит на тестовый ящик, ссылки верны |
| Уведомление об оплате | Кабинет провайдера | Входящий запрос с подписью | Маршрут нового веб-процесса | Тестовая оплата меняет статус заказа |
| Закрытие неоплаченных | Расписание платформы или приложения | Таймер | systemd timer или cron | Ручной запуск закрывает только нужные заказы |
Теперь можно думать о размере машины, и только теперь. Весь контур из этой карты — веб-процесс, обработчик очереди, база, брокер — на одном VPS потребляет суммарную память и процессор, а не по отдельности. Тарифы MAATRIX начинаются с Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и идут через Uab (2 vCPU, 4 ГБ, 80 ГБ) до Heka (4 vCPU, 8 ГБ, 160 ГБ) и выше, но эти цифры описывают ресурсы, а не ёмкость: сколько заказов выдержит ваш магазин, покажет нагрузочная проверка копии, а не таблица тарифов. С root-доступом вы сами ставите базу, брокер и службы и сами отвечаете за их обновления, резервные копии и мониторинг. Если у машины будет жить и база, и очередь, и приложение, полезно сразу разделить их по пользователям и каталогам, чтобы при росте каждую часть можно было вынести отдельно. Расположение сервера (в предложении указаны UK, US, RU и FR) тоже стоит выбирать по задаче: ближе к пользователям, ближе к базе, которую вы оставляете, или с учётом требований к размещению данных, которые вам нужно проверить отдельно.
Репетиция и критерии возврата
Чтобы карта превратилась в уверенность, прогоните по ней репетицию на отдельной копии, а не на рабочем домене. Разверните окружение на VPS, подключите тестовые ключи, тестовую базу и тестовый ящик, выключите расписания либо направьте на тестовые данные и пройдите путь заказа от кнопки до квитанции. Результат фиксируйте по каждому шагу: прошёл или нет и что именно наблюдалось.
Заранее решите, что считать поводом не переключать трафик или вернуть его обратно. Критерии должны быть наблюдаемыми, а не «если что-то пойдёт не так».
- Тестовый заказ не доходит до записи в базе, или запись есть, но обработчик очереди не получает задачу.
- Квитанция создаётся, но ссылка из письма не открывается либо перестаёт открываться после перезапуска службы.
- Уведомление об оплате не доходит до нового окружения или не проходит проверку подписи.
- Письмо не принимается получателем, а ссылки в нём указывают на старый технический адрес.
- Ночная задача при ручном запуске затрагивает не те заказы или срабатывает дважды.
Пока DNS-запись указывает на старую платформу, а старое окружение не удалено, возврат состоит из одного шага: вернуть запись. Именно поэтому старую платформу не выключают сразу после переключения. Её оставляют работающей, пока не пройдёт хотя бы один полный цикл самой длинной фоновой задачи и не придут первые настоящие уведомления об оплате. Важный нюанс: заказы, созданные в новом окружении за время пробной работы, должны либо быть видны старому окружению (общая база), либо быть перенесены при возврате. Если базы разные, эту развилку нужно продумать заранее, потому что после возврата клиенты не должны потерять свои заказы.
Возвращаясь к кнопке
Теперь снова нажмём «Оформить заказ», только уже в новом окружении. Форма доходит до веб-процесса на VPS. Заказ записывается в базу, адрес которой лежит в защищённом файле окружения. Задача попадает в очередь, и обработчик, запущенный отдельной службой, забирает её. Квитанция записывается в каталог, о существовании которого вы знаете, и в ссылке из письма стоит ваше имя домена, а не технический адрес прежней платформы. Письмо уходит с подтверждённого домена. Платёжный провайдер присылает уведомление на адрес, который вы проверили в его кабинете, и статус заказа меняется. А ночью таймер закроет неоплаченное ровно один раз.
Ничего из этого не лежало в архиве с кодом. Всё это нашлось потому, что мы шли за заказом, а не за списком сервисов. Когда новое окружение воспроизводит весь путь этой кнопки, переезд можно считать готовым к настоящему трафику.