Основная копия уже лежит на новом сервере. Каталог аккуратный, размеры похожи, приложение на тестовом адресе открывает старые картинки. И в этот момент на старой площадке кто-то прикрепляет договор к заявке, кто-то заменяет аватар, а скрипт ночной чистки удаляет временный файл. Между двумя серверами появляется небольшой, но важный зазор: ничего не сломано, просто копия начала стареть, пока вы на неё смотрите.

Эта статья про то, как закрыть такой зазор намеренно, а не надеяться, что он окажется пустым. Мы разберём, как найти, какие пути приложения действительно живут на Railway Volume, как разделить долгую первичную копию и короткую финальную синхронизацию, как выставить владельцев и права на новом сервере и чем доказывать полноту копии. Отдельно поговорим о связи файлов с состоянием базы данных, потому что запись в таблице и файл на диске должны указывать друг на друга. Саму миграцию базы мы здесь не разбираем; общую картину переезда приложения, базы, очереди и файлов можно посмотреть в статье Как перенести приложение с Railway на VPS: база, очередь и файлы.

Сначала выясняем, что именно лежит на volume

Первая ошибка переноса файлов совершается до единой команды: мы предполагаем, что знаем, где приложение хранит данные. Знаем мы обычно, где должно хранить. Фактически же в одном сервисе могут сосуществовать загрузки пользователей, кэш миниатюр, сгенерированные отчёты, файл SQLite, который кто-то когда-то подключил «временно», и каталог с логами. Переносить нужно не всё, а то, что нельзя восстановить из кода и базы.

По документации Railway, volume подключается к сервису по заданному mount path и выглядит для приложения как обычный каталог. Из этого следует практическое правило: отправной точкой служит не имя volume в панели, а mount path в настройках сервиса. Всё, что приложение пишет под этим путём, попадает на volume. Всё, что пишет мимо него, живёт в файловой системе контейнера и исчезнет при следующем развёртывании, переносить это бессмысленно, хотя повод задуматься о таких записях есть.

Вторая половина инвентаризации сложнее. Код приложения чаще оперирует относительными путями: uploads/2024/05/file.pdf, media/avatars/42.png. Относительный путь значит что-то лишь в связке с рабочим каталогом процесса или с настройкой вроде корня медиафайлов. Документация Railway прямо требует сопоставлять относительный путь записи приложения с фактическим абсолютным путём volume. Поэтому полезно составить маленькую таблицу соответствий до начала копирования.

Пример инвентаризации путей (условный сервис с загрузками)
Что видит кодАбсолютный путь на RailwayНужен ли переносПуть на VPS
uploads/mount path + /uploadsда, это данные пользователей/srv/app/data/uploads
thumbs/mount path + /thumbsнеобязательно, можно пересоздать/srv/app/data/thumbs
exports/mount path + /exportsзависит от срока хранения отчётов/srv/app/data/exports
app.logвне volumeнетжурнал ведётся заново

Колонка «Нужен ли перенос» вынуждает принять решение по каждой строке. Миниатюры, которые приложение умеет генерировать заново, можно не тащить, но тогда первые минуты после переезда будут медленнее: каждая картинка пересоздаётся при первом запросе. Это нормальная цена, если вы её осознаёте. А вот перенос «на всякий случай» всего подряд раздувает время финальной синхронизации и размывает сверку.

Как найти пути, о которых забыли

Хороший способ проверки: посмотреть, что реально лежит под mount path, и сопоставить с тем, о чём вы знаете. Документация Railway CLI описывает просмотр, загрузку и скачивание файлов volume, так что листинг можно получить без обходных схем. Если в каталоге обнаружился подкаталог, который не упоминается в коде, найдите, кто его пишет. Иногда это библиотека с собственным кэшем, иногда старая функция, о которой все забыли, но чьи файлы ещё лежат в базе в виде ссылок.

Есть и обратная проверка, из базы. Если приложение хранит в таблице вложений путь или ключ файла, выберите из неё набор уникальных префиксов путей и сравните с каталогами на диске. Префикс в базе, которого нет на диске, означает уже потерянные файлы или другой источник хранения, например внешний объектный бакет. Каталог на диске, которого нет в базе, часто оказывается мусором или кэшем. Обе находки полезно получить до миграции, а не после.

Две копии вместо одной: длинная и короткая

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

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

Вторая фаза — финальная синхронизация после остановки записей. Она короткая, потому что передаёт только разницу: файлы, появившиеся и изменившиеся с момента первичной копии. Именно здесь копия становится точной, и именно от этой фазы зависит ваше окно простоя.

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

Способ экспорта зависит от того, что доступно вам

Здесь нужна осторожность. Документация описывает работу с файлами volume через Railway CLI, и это подтверждённый путь для выгрузки и загрузки. Но не стоит исходить из того, что на любом аккаунте можно зайти по SSH внутрь контейнера и запустить там rsync прямо к новому серверу. Возможности платформы меняются, и перед планом переноса нужно проверить, какой способ экспорта доступен именно вам сейчас. Практичная схема, не зависящая от внутреннего доступа: выгрузить данные на промежуточную машину (рабочую станцию или то же VPS) средствами, которые даёт платформа, и уже оттуда выполнять повторяемые синхронизации на целевой сервер.

Это важное разделение. Платформенный экспорт — шаг, который может быть медленным и не всегда инкрементальным. Локальная синхронизация между двумя вашими машинами — инструмент, который умеет передавать только отличия и сравнивать содержимое. Если ваш способ выгрузки каждый раз скачивает всё заново, финальное окно будет равно полному времени скачивания, и тогда стоит считать его заранее, а не обнаруживать в день переезда.

Зазор между копиями и как его закрыть

Вернёмся к сцене из начала. Пользователи работают, файлы добавляются и меняются. Какие виды расхождений возможны после первичной копии? Их ровно четыре, и каждый требует своего поведения от синхронизации.

  • Новый файл. Появился на старом месте после копии. Синхронизация должна его добавить. Это самый частый случай и самый безобидный.
  • Изменённый файл. Имя то же, содержимое другое. Если сравнивать только по имени и размеру, замена картинки на другую того же размера останется незамеченной. Здесь пригодится сравнение по времени изменения и, при сомнениях, по контрольной сумме.
  • Удалённый файл. На старом месте его уже нет, на новом ещё лежит. Что делать, зависит от смысла: удалил ли его пользователь или это очистка временных данных.
  • Файл в процессе записи. Копия застала его на середине, и на новом сервере лежит обрезанный. Это самое коварное расхождение, потому что размер и имя выглядят правдоподобно. Спасает только повторный проход уже после остановки записей.

Удаление при синхронизации: направление важнее флага

Многие инструменты синхронизации умеют удалять в целевом каталоге то, чего нет в источнике. Для зеркала это логично, для переезда опасно. Представьте, что вы по ошибке перепутали пути: источником указан пустой или частично выгруженный каталог, а целью — полная копия. Режим удаления честно сделает целевой каталог таким же пустым. Поэтому слепое зеркалирование с удалением в миграции — плохая привычка, а не экономия времени.

Разумная политика такая. В первичных проходах удаление отключено совсем: лишний файл на новом сервере безвреден, потерянный файл — нет. Перед финальным проходом выполните пробный запуск в режиме «только показать, что изменится». Прочитайте список целиком, особенно строки об удалении. Если там сотни файлов, которых вы не ждали, вы перепутали направление или выгрузка неполна. Только после осмысленного просмотра включайте удаление, и то лишь если оно действительно нужно: например, пользователи удаляли файлы в последние дни и вы не хотите воскрешать их на новом сервере. Если сомневаетесь, оставьте лишние файлы и вычистите их отдельно потом, когда всё работает.

Есть и тонкость с самой постановкой направления. Прежде чем запускать, произнесите вслух: «источник — это выгрузка со старой платформы, цель — каталог данных на VPS». Звучит как шутка, но перепутать порядок двух аргументов в командной строке проще, чем кажется, особенно ночью.

rsync -avn --itemize-changes /srv/export/uploads/ /srv/app/data/uploads/

Ключ -n означает пробный прогон: ничего не изменится, вы увидите только план. Слэш в конце пути источника тоже имеет значение: с ним копируется содержимое каталога, без него — сам каталог внутри цели. Это стандартное поведение rsync, и именно на нём спотыкаются, получая вложенный uploads/uploads. Команда приведена как пример для промежуточной машины с выгрузкой, а не как способ работы внутри Railway.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Новый сервер: владелец, права и ожидаемые пути

Файлы на VPS лежат, но приложение может их не увидеть или не суметь записать рядом новые. Причина почти всегда в одном из трёх: другой владелец, другие права или другой путь.

Владелец. На платформе процесс приложения работал от определённого пользователя внутри контейнера. На VPS вы запускаете его от своего пользователя или от выделенного служебного, чей числовой идентификатор может быть любым. Если файлы скопированы под root, приложение, запущенное под обычным пользователем, сможет их читать (если права позволяют), но не сможет изменять или создавать рядом новые. Симптом знакомый: старые вложения открываются, а загрузка нового падает с ошибкой доступа. Для этого заведите отдельного пользователя под приложение и назначьте его владельцем каталога данных.

sudo chown -R appuser:appuser /srv/app/data
sudo find /srv/app/data -type d -exec chmod 750 {} +
sudo find /srv/app/data -type f -exec chmod 640 {} +

Значения 750 и 640 здесь пример разумной строгой схемы: владелец читает и пишет, группа читает, остальные не имеют доступа. Если за выдачу файлов отвечает отдельный веб-сервер, он должен входить в нужную группу или получать файлы через приложение. Подбирайте права под свою схему, а не копируйте цифры вслепую. Разделение прав на каталоги и файлы нужно потому, что у каталога бит исполнения означает возможность входа, а у обычного файла он не нужен.

Пути. Приложение теперь ищет файлы не по mount path Railway, а по каталогу на VPS. Обычно достаточно сменить переменную окружения с корнем хранилища. Но если в базе лежат абсолютные пути, записанные на старой платформе, они продолжат указывать на несуществующий каталог. Проверьте это запросом к таблице вложений: если в колонке встречается префикс старого mount path, нужно либо создать на новом сервере такой же путь (символическая ссылка или каталог с тем же именем), либо аккуратно переписать значения. Второй вариант чище, но трогает базу, поэтому делайте его только после резервной копии таблицы.

Заодно оцените, сколько места нужно. Каталог данных должен уместиться на диске с запасом под рост, временные промежуточные выгрузки и сами рабочие нужды системы. Ориентир по ресурсам удобно искать в тарифной линейке: у VPS MAATRIX диски NVMe различаются по тарифам (от 40 ГБ у Ba до 320 ГБ у Djed), но эти цифры описывают объём диска, а не готовность под вашу нагрузку; сначала измерьте размер выгруженных данных и умножьте на разумный коэффициент запаса, например на два, если промежуточная копия лежит на том же диске. Это условный расчёт: подставьте свои числа. Если и данные, и промежуточная выгрузка на одной машине, объём нужен под оба набора одновременно, пока вы не удалите черновую копию.

Чем доказывать полноту копии

Теперь главный вопрос: как убедиться, что на новом сервере лежит именно то, что было на старом? Привычный ответ «посчитал файлы, число совпало» годится как первый сигнал и никак не как доказательство. Число файлов совпадёт, если один файл оказался обрезанным, если два файла поменялись содержимым или если один потерялся, а один лишний добавился. Количество говорит лишь о количестве.

Надёжнее работает лестница проверок, где каждая следующая ступень строже предыдущей.

  1. Количество и суммарный размер. Быстрая грубая проверка. Несовпадение сразу указывает на проблему, совпадение пока ничего не доказывает.
  2. Список путей. Сравнение отсортированных списков относительных путей двух каталогов. Это ловит потерянные и лишние файлы, даже когда их число случайно совпало.
  3. Размеры по каждому файлу. Список «путь и размер». Ловит обрезанные файлы, пойманные посреди записи.
  4. Контрольные суммы. Хеш содержимого каждого файла. Ловит всё, включая замену содержимого при том же размере.

Как сделать сверку по суммам

Идея проста: на каждой стороне построить манифест, то есть файл, где у каждого пути записана контрольная сумма, и сравнить два манифеста как обычные текстовые файлы. Важно, чтобы пути в манифестах были относительными от корня каталога, иначе разные префиксы дадут ложные различия, а сортировка была одинаковой.

cd /srv/export/uploads && find . -type f -print0 | sort -z | xargs -0 sha256sum > /tmp/source.sha256
cd /srv/app/data/uploads && find . -type f -print0 | sort -z | xargs -0 sha256sum > /tmp/target.sha256
diff /tmp/source.sha256 /tmp/target.sha256 && echo совпадает

Пустой вывод diff означает идентичные манифесты: тот же набор путей и то же содержимое. Если различия есть, diff покажет строки: путь, которого нет с одной стороны, или тот же путь с другой суммой. Обратите внимание на ключи -print0 и -0: они нужны, чтобы имена с пробелами и необычными символами, а пользовательские вложения ими богаты, не ломали обработку.

Есть важное уточнение о том, что именно сравнивается. Манифест источника нужно строить по той копии, которую вы реально получили после экспорта, а не по живому каталогу на Railway, куда продолжают писать. Если же ваш способ выгрузки позволяет снять манифест прямо на старой стороне после остановки записей, это даже лучше: тогда вы сравниваете истинное состояние источника с целью. Какой вариант доступен, зависит от возможностей вашего аккаунта и способа экспорта, поэтому закладывайте оба и выбирайте рабочий.

У rsync есть режим сравнения по контрольной сумме (--checksum), и пробный прогон с ним тоже годится как сверка: если он не показывает изменений, содержимое совпадает. Но манифест полезнее по другой причине: это артефакт. Его можно сохранить, приложить к записи о переезде и вернуться к нему, если через неделю кто-то скажет, что вложение «раньше было другим». Такой документ дороже одноразового «вроде всё совпало».

Что делать с расхождениями

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

Файл и строка в базе: одна точка во времени

Теперь о самом тонком. Приложение хранит файл на диске, а сведения о нём (имя, владельца, привязку к заявке) в базе. Пара «строка и файл» целостна только тогда, когда обе части относятся к одному моменту. Копирование файлов работающего приложения само по себе этого не гарантирует. Если вы скопировали каталог в 14:00, а дамп базы сняли в 14:20, то в базе окажутся записи о вложениях, чьих файлов нет на диске, потому что они загружены между двумя снимками. Обратная ситуация тоже возможна: файл лежит, а строки в базе нет, и он превращается в сироту.

Поэтому момент переключения файлов нужно привязать к состоянию базы. Рабочая последовательность выглядит так.

  1. Остановить запись: перевести приложение в режим обслуживания или остановить сервис, так чтобы новые загрузки и изменения не принимались.
  2. Выполнить финальную синхронизацию файлов и сверку манифестов.
  3. Снять финальное состояние базы уже после остановки и перенести его, пользуясь процедурой, описанной в вашем плане миграции базы.
  4. Запустить приложение на VPS с обеими частями, относящимися к одной точке остановки.

Ключевая мысль в том, что остановка записи одна, и оба снимка берутся после неё. Тогда и файлы, и база описывают одно и то же состояние. Сам перенос базы остаётся отдельной задачей; здесь важно лишь, чтобы он не опережал файлы и не отставал от них.

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

Проверка согласованности файлов и записей

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

Если подобный скрипт нужно запускать на самом VPS, ему достаточно доступа на чтение к базе и к каталогу данных. Нагрузка небольшая, но на объёме в сотни тысяч файлов проверка существования займёт заметное время; запускайте её вне пиковых часов и не на той же минимальной конфигурации, где уже работают приложение и база, если ресурсов впритык. Тариф стоит подбирать под измеренную нагрузку, а не под цифры из каталога.

Где на VPS живут файлы после переезда

Когда перенос закончен, уместно подумать, как организовать хранение дальше. На VPS с root-доступом вы сами решаете, где лежат данные и как они защищены. Это свобода и ответственность одновременно: платформа больше не подключает volume за вас. Каталог данных можно вынести на отдельный раздел или диск, чтобы переполнение журналов не съело место под пользовательские файлы, а резервное копирование настроить отдельно от кода. Подтверждённых автоматических резервных копий во всех тарифах в публичном предложении нет, поэтому план копирования файлов придётся предусмотреть самостоятельно, и лучше сделать это до того, как на новом месте появятся ценные вложения, а не после.

Если вашему сценарию нужна не только отдача файлов приложением, но и самостоятельное файловое хранилище с веб-интерфейсом, в публичном каталоге VPS MAATRIX среди предложений с установкой указан Nextcloud; точное наличие и операционную систему образа стоит уточнить в форме заказа. Это другая архитектура, а не замена переносу volume, и подходит она лишь тогда, когда вам действительно нужен такой продукт. Для большинства приложений достаточно обычного каталога на NVMe-диске под управлением вашего процесса. Поскольку у VPS есть доступ root и несколько локаций (в предложении перечислены UK, US, RU и FR), расположение сервера лучше выбирать исходя из того, где находятся ваши пользователи и сервисы, с которыми приложение обменивается данными.

Репетиция, которая экономит ночь

Всё описанное разумно прогнать целиком на тестовом каталоге до реального дня. Репетиция не требует остановки продукта: достаточно выполнить первичную выгрузку, одну промежуточную синхронизацию и построить манифесты. Вы узнаете, сколько занимает каждый шаг, как ведёт себя ваш способ экспорта, нет ли файлов с неудобными именами и совпадают ли права. Самое ценное, что даёт репетиция, — длительность финальной синхронизации в реальных цифрах. Именно она определяет, сколько минут приложение будет закрыто для записи, и этот срок можно честно озвучить пользователям заранее.

Заранее договоритесь, какой результат считается успехом и что вы будете делать, если сверка не сошлась. Пока старая площадка не выключена, возврат прост: направить трафик обратно. Не удаляйте volume на Railway сразу после переключения. Оставьте его нетронутым на период наблюдения, пока не убедитесь, что новые вложения создаются, старые открываются, а манифесты сохранены. Удаление источника — единственное необратимое действие во всей процедуре, и его стоит делать последним.

Свежее вложение как последняя проверка

Когда манифесты совпали, права выставлены, а приложение запущено на VPS, остаётся проверка, которую скрипт заменить не может. Найдите в базе самое свежее вложение, созданное незадолго до остановки записей: то самое, что появилось уже после первичной копии и жило в зазоре между серверами. Откройте его в интерфейсе приложения под учётной записью, которой оно принадлежит. Если файл открылся целым, с правильным содержимым, а следом вы загрузили новое вложение и увидели его на диске нового сервера под ожидаемым владельцем, зазор закрыт. Старые файлы доказывают, что основная масса доехала, а самый свежий — что вы не потеряли ничего из того, что происходило последним.