На панели App Platform есть кнопка Deploy, и со стороны кажется, что она запускает одно приложение. На деле за ней часто стоит маленький оркестр: веб-процесс, который отвечает на запросы, фоновый работник, который разбирает очередь, и задание по расписанию, которое раз в сутки что-то пересчитывает или чистит. Платформа собирала их вместе, следила за ними и перезапускала, а вы видели один зелёный индикатор. Когда вы переезжаете на обычный VPS, этот индикатор исчезает, и каждую роль приходится назвать по имени и поселить отдельно.
Эта статья построена вокруг одного условного приложения. Это сайт, на который пользователи загружают файлы (например, документы или изображения), обработчик, который эти файлы проверяет и преобразует, и ежедневное задание, которое готовит сводку и удаляет устаревшие временные записи. Состав выдуманный, но он хорошо показывает, где при переезде прячутся ловушки. Начнём с процессов, затем проследим путь одного файла, потом разберёмся, кто теперь отвечает за перезапуск и секреты, нарисуем сетевую схему и закончим проверкой, которая доказывает, что переезд состоялся, а не только запустился.
Три процесса под одной кнопкой
Первое, что стоит сделать, — выписать, из каких компонентов состоит приложение в панели. Нам нужны не красивые названия, а ответы на три вопроса по каждому компоненту: какая команда его запускает, на какие события он реагирует и что ему нужно снаружи.
| Роль | Что делает | Как живёт на VPS |
|---|---|---|
| Веб-процесс | Принимает HTTP-запросы, принимает загрузки | Контейнер за обратным прокси, порт наружу не опубликован |
| Фоновый работник | Берёт задачи из очереди, обрабатывает файлы | Отдельный контейнер из того же образа, другая команда запуска |
| Задание по расписанию | Раз в сутки считает сводку, чистит записи | Короткоживущий запуск по таймеру или cron на хосте |
Обратите внимание: веб и работник часто собираются из одного кода и даже из одного образа, а различаются только командой. Это удобно. Вы собираете образ один раз и запускаете его в разных ролях. Задание по расписанию устроено иначе: оно не должно висеть постоянно, ему нужно стартовать, сделать работу и завершиться. Если запустить его как обычный долгоживущий сервис с политикой перезапуска, оно будет бесконечно стартовать заново после каждого успешного завершения. Это классическая ошибка при переносе: расписание превращается в бесконечный цикл.
Как описать это в Compose
Для одного VPS естественный инструмент — Compose-файл. Документация Docker отмечает, что одну и ту же конфигурацию можно использовать в CI, staging и production, меняя настройки под окружение, а для production, как правило, нужно убрать bind mount с кодом, поменять порты и переменные окружения и добавить политику перезапуска (Docker Compose: Production). Для нашего приложения это означает, что код живёт в образе, а не монтируется с диска разработчика, и что у долгоживущих сервисов есть явная политика перезапуска.
services:
web:
image: registry.example.com/shop-app:2026-11-01
command: ["./run-web"]
restart: unless-stopped
env_file: /etc/shop-app/app.env
expose:
- "8080"
depends_on:
- redis
worker:
image: registry.example.com/shop-app:2026-11-01
command: ["./run-worker"]
restart: unless-stopped
env_file: /etc/shop-app/app.env
depends_on:
- redis
redis:
image: redis:7
restart: unless-stopped
Имена образа, команд и версий здесь условные: подставьте свои. Важна структура. Веб и работник используют один образ, но разные команды. Порт у веба объявлен через expose, то есть он доступен другим контейнерам в сети Compose, но не опубликован на внешний интерфейс сервера. Наружу будет смотреть только прокси, о нём ниже. Задания по расписанию в этом файле нет намеренно, и сейчас объясню почему.
Расписание: два честных варианта
Ежедневное задание можно запускать двумя способами, и оба рабочие.
- Cron или systemd timer на хосте вызывает одноразовый запуск контейнера:
docker compose run --rmс нужной командой. Расписание живёт в системе, контейнер создаётся на время работы и удаляется. - Отдельный сервис-планировщик в самом Compose, который сам знает время и дёргает нужную команду. Это ещё один процесс, который нужно мониторить, зато расписание лежит рядом с остальной конфигурацией.
Для небольшого приложения проще первый вариант. Системный таймер умеет то, что на платформе вы получали по умолчанию, но теперь должны включить сами: записывать в журнал каждый запуск, не стартовать повторно, если предыдущий ещё идёт, и догонять пропущенный запуск после перезагрузки сервера. Последнее особенно важно. Если VPS был перезагружен ровно в момент, когда задание должно было сработать, обычный cron просто пропустит этот день, а таймер systemd с настройкой догоняющего запуска выполнит его при старте. Решите заранее, какое поведение вам нужно: для ежедневной сводки пропуск может быть безобиден, а для списания остатков или рассылки — нет.
И ещё одна деталь про одноразовые запуски. Задание должно быть идемпотентным настолько, насколько это возможно: если оно случайно сработает дважды (скажем, вы запустили его вручную при проверке, а потом пришёл таймер), повторный запуск не должен ломать данные. Это свойство самого задания, не инфраструктуры, но при переезде оно впервые проверяется на практике: на платформе дублирующие запуски исключала её собственная логика, а теперь за это отвечаете вы.
Путь одного файла: где он лежит на самом деле
Теперь самая опасная часть. Пользователь загружает документ. Веб-процесс принял его, положил куда-то и поставил в очередь задачу на обработку. Работник взял задачу, нашёл файл, преобразовал и сохранил результат. Вопрос в том, что значит «куда-то» и «нашёл».
Документация App Platform описывает это однозначно: локальная файловая система экземпляра эфемерна и теряется после redeploy или замены контейнера, а у разных экземпляров приложения файловые системы раздельные. Для постоянных данных предлагаются object storage или managed database, а volumes платформа на дату проверки не поддерживает (DigitalOcean App Platform: Store data). Из этого следует важное практическое наблюдение: если приложение работало на платформе корректно, то оно либо уже хранит загрузки во внешнем хранилище, либо работало «почти правильно», пока не случился redeploy. Поэтому первый шаг при переезде — не копировать каталог, а выяснить, что в коде делает функция сохранения файла.
Три возможных ситуации
- Код пишет загрузки в object storage. Тогда на VPS ничего не меняется: те же ключи доступа, тот же бакет. Переезд затрагивает только процессы. Это самый спокойный вариант, и файлы вообще не двигаются.
- Код пишет в локальный каталог контейнера, а данные считались временными. Например, это промежуточные файлы, которые работник удаляет после обработки. Тогда на VPS их можно оставить в временном каталоге, но помните про второй пункт ниже.
- Код пишет в локальный каталог, а данные на самом деле нужны постоянно. Это скрытая ошибка, которую переезд просто обнажает. На платформе такие файлы терялись при замене контейнера, и, возможно, пользователи уже сталкивались с исчезнувшими загрузками. Прежде чем переносить, решите, куда эти данные должны идти теперь.
Почему локальный каталог контейнера на VPS тоже не хранилище
Соблазн понятен: на VPS у вас есть настоящий диск, значит, можно писать куда угодно. Но каталог внутри контейнера всё равно исчезает, когда контейнер пересоздаётся, а по документации Compose это обычная ситуация: обновление кода в образе требует пересборки образа и пересоздания контейнера, при этом отдельный сервис можно обновить, не трогая его зависимости (Docker Compose: Production). Каждый ваш релиз веб-сервиса — это новый контейнер. Если загрузки лежали в его собственной файловой системе, они уйдут вместе со старым контейнером.
Поэтому на VPS у вас два нормальных пути для постоянных файлов. Первый — смонтировать том или каталог хоста в контейнеры, которым нужен доступ. Второй — оставить внешний object storage, как и было. Выбор зависит от того, что вам важнее. Локальный том проще и быстрее, но привязывает данные к одному серверу: если веб и работник живут на одной машине, они увидят один и тот же каталог, а если вы когда-нибудь захотите вынести работника на другой сервер, общий каталог придётся заменить на что-то сетевое. Object storage избавляет от этой привязки ценой дополнительной зависимости и настройки доступа.
Для нашего примера на одном VPS разумно сделать так: загрузки пользователей и результаты обработки лежат в смонтированном каталоге хоста, который веб и работник видят по одинаковому пути. Тогда веб-процесс сохраняет файл, работник находит его по тому же пути, а пересоздание любого контейнера данные не затрагивает. Но это решение должно сопровождаться резервным копированием каталога: том переживает пересоздание контейнера, но не переживает потерю диска или ошибочное удаление.
web:
volumes:
- /srv/shop-app/uploads:/data/uploads
worker:
volumes:
- /srv/shop-app/uploads:/data/uploads
Одинаковый путь внутри обоих контейнеров — не прихоть. Если веб записал в очередь абсолютный путь к файлу, работник должен открыть именно его. На платформе с несколькими экземплярами такая схема сломалась бы сразу, потому что у каждого экземпляра своя файловая система. Если у вас в коде когда-то были костыли вроде «передать файл в теле задачи» из-за этого ограничения, на VPS их можно упростить, но только осознанно и после проверки.
Права доступа: незаметная причина «файл не найден»
Каталог на хосте принадлежит какому-то пользователю, а процесс внутри контейнера работает под своим идентификатором. Если они не совпадают, веб-процесс не сможет записать загрузку, а работник не сможет её прочитать, и вы получите ошибку, которой не было на платформе. Проверьте, под каким пользователем запускается образ, и выдайте каталогу владельца с таким же числовым идентификатором. Это скучная мелочь, но именно она чаще всего ломает первый тестовый запуск.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Кто теперь следит, чтобы всё жило
На платформе вы не думали о том, что процесс может упасть. Он упал, платформа его подняла. На VPS это ваша забота, и инструментов тут несколько уровней.
Первый уровень — политика перезапуска контейнера. Документация Compose прямо называет её одной из типичных production-настроек (Docker Compose: Production). Вариант unless-stopped поднимет контейнер после сбоя и после перезагрузки сервера, если вы сами его не останавливали. Второй уровень — сам демон контейнеров: он должен стартовать при загрузке системы, иначе политика перезапуска не сработает, потому что некому её исполнять. Это проверяется одной командой управления службами и один раз включается. Третий уровень — наблюдение: перезапуск скрывает сбой, поэтому контейнер, который падает каждые десять секунд, выглядит «живым». Нужен хотя бы простой способ увидеть счётчик перезапусков и журналы.
Заметим, чего на VPS нет само по себе. Платформа могла масштабировать приложение на несколько экземпляров, управляла сертификатами и держала резервирование. Один VPS этого не воспроизводит: если упадёт сам сервер, упадёт всё приложение, а сертификаты и их продление придётся настраивать. Это не недостаток, а условия сделки: вы получаете предсказуемую машину и полный контроль, но резервирование и автоматику добавляете сами или сознательно обходитесь без них.
Секреты и переменные окружения
В панели платформы вы хранили ключи доступа, адреса баз данных и токены в настройках приложения. На VPS они переезжают в файл окружения или другой механизм хранения. В примере выше это /etc/shop-app/app.env. Несколько практических правил, которые вытекают из самой ситуации:
- Файл с секретами не лежит в репозитории и не попадает в образ. Образ должен быть пригоден к запуску с любым набором значений.
- Права на файл ограничены: читать его должен только тот, кто запускает контейнеры.
- Список переменных стоит выписать из панели до переезда целиком. Забытая переменная проявится не сразу, а, например, когда ежедневное задание впервые попытается отправить сводку.
- Значения, которые на платформе подставлялись автоматически (например, адрес подключённой базы), на VPS придётся задать явно. Сверьте, какие переменные платформа создавала сама.
Ежедневное задание использует тот же файл окружения, что и остальные. Если запускать его с хоста через таймер, убедитесь, что оно получает те же переменные. Типичная ошибка: из терминала всё работает, а из таймера нет, потому что окружение у него другое.
Сетевая схема: одна дверь вместо россыпи адресов
На платформе у приложения был публичный адрес и, возможно, внутренние адреса компонентов. На VPS схему лучше сделать максимально простой, чтобы её можно было нарисовать в одну строку: интернет приходит на обратный прокси, прокси передаёт запросы веб-контейнеру, а веб, работник и очередь общаются между собой внутри сети Compose.
- Наружу открыты только порты 80 и 443 (и порт управления для вашего доступа к серверу). Веб-контейнер, очередь и база не публикуют порты на внешний интерфейс.
- Прокси завершает TLS. На платформе сертификатом занималась она; теперь это обязанность прокси и его настройки автоматического продления. Сертификат, который тихо истёк, снаружи выглядит как полное падение сайта.
- Внутренние имена сервисов (
web,redis) заменяют внутренние адреса платформы. Проверьте, что в конфигурации приложения нет жёстко прописанных старых адресов. - Базу данных решите отдельно. Если она была управляемой и остаётся на старом месте, вам нужно разрешить обращение с нового сервера и подумать о задержке между площадками. Если вы переносите её на VPS, это самостоятельная задача со своим планом.
Если домен приложения смотрит на старый адрес, переключение трафика — это смена DNS-записи. Заранее уменьшите время жизни записи, чтобы переключение и, при необходимости, возврат происходили быстро. Пока записи не обновились у всех, часть пользователей ещё будет заходить на старое место, и это нормально, но значит, что оба окружения какое-то время должны работать с одними и теми же данными. Для нашего приложения это проблемное место: если загрузки лежат в локальном каталоге нового сервера, а часть пользователей ещё приходит на старую платформу, файлы окажутся в разных местах. Поэтому либо данные должны быть в общем хранилище, доступном обоим окружениям, либо переключение нужно планировать как короткое окно с остановкой записи.
Сам сервер — это ресурс, который вы подбираете под нагрузку. В публичном предложении VPS MAATRIX указаны NVMe-диски, процессоры AMD EPYC и root-доступ, а тарифы различаются по vCPU, памяти и диску: от Ba с 1 vCPU, 2 ГБ RAM и 40 ГБ NVMe до Djed с 16 vCPU, 32 ГБ и 320 ГБ. Эти цифры описывают ресурсы, а не гарантированную ёмкость: сколько загрузок в минуту выдержит ваш работник, покажет только замер на вашем приложении. Для нашего примера из трёх процессов и очереди важны две вещи: память (обработка файлов может быть прожорливой) и диск (загрузки копятся). Оба параметра проще увеличить заранее, чем разбирать последствия нехватки. Локации в предложении перечислены: UK, US, RU и FR; точная доступность образов и операционных систем проверяется в форме заказа.
Репетиция: как доказать, что переезд сработал
Запущенные контейнеры ещё ничего не доказывают. Проверка должна воспроизвести то, что в обычной жизни ломается: перезапуск, замену контейнера и полный цикл с файлом. Вот последовательность, которая подходит для нашего примера.
- Сквозная загрузка. Загрузите через сайт тестовый файл. Убедитесь, что он появился в каталоге хранения, что работник взял задачу, обработал и записал результат, и что пользователь видит итог.
- Пересоздание контейнеров. Остановите и создайте заново веб и работник (как при релизе). Загруженный файл и результат обработки должны остаться на месте. Если они исчезли, значит, где-то остался локальный каталог контейнера, выданный за хранилище.
- Перезагрузка сервера. Перезапустите VPS целиком. Проверьте, что демон контейнеров поднялся, сервисы встали сами, сертификат отдаётся, очередь не потеряла задачи (если это важно для вашего приложения).
- Аварийный сбой процесса. Принудительно завершите работника. Он должен вернуться, а задача, которая была в обработке, — либо продолжиться, либо корректно повториться. Что именно произойдёт, зависит от вашей очереди, и это нужно знать заранее.
- Задание по расписанию. Запустите его вручную и проверьте результат и журнал. Затем убедитесь, что таймер зарегистрирован и показывает ближайший запуск. Отдельно проверьте, что повторный ручной запуск не портит данные.
- Резервная копия и восстановление. Скопируйте каталог загрузок и данные базы, затем попробуйте восстановить их на чистом окружении. Копия, из которой ни разу не восстанавливались, остаётся предположением.
Критерии переключения лучше записать до начала, а не придумывать в момент, когда все устали. Например: сквозная загрузка проходит, пересоздание контейнеров не теряет файлы, задание отработало на тестовых данных, сертификат валиден, ошибок в журналах за выбранный вами период нет. И так же заранее запишите критерий возврата: при каких признаках вы возвращаете DNS на платформу. Пока старое окружение не удалено и данные синхронизированы, возврат — это смена записи, а не авария. После удаления старого приложения такой путь закрыт, поэтому не спешите с удалением.
Если вам нужен общий порядок переезда самого сервера, а не конкретно App Platform, есть отдельный разбор: как перенести сайт и сервер с DigitalOcean на свой VPS. В этой статье мы разобрали именно то, что превращает приложение на платформе в набор ролей на обычной машине.
Что было кнопкой, а стало задачей
Переезд полезно читать как перенос ответственности. Кнопка Deploy раньше решала сразу несколько вопросов: где запускать процессы, кто их перезапускает, где лежат файлы, как выдаётся сертификат, откуда приходят секреты. Теперь каждый из них имеет конкретное место на вашем сервере: Compose-файл, политика перезапуска, том или object storage, прокси с продлением сертификата, файл окружения с ограниченными правами.
Какие обязанности исчезли из панели и появились у администратора? Пройдите по списку и для каждой назовите, где она теперь живёт и как вы узнаете, что она перестала выполняться. Если на какой-то пункт ответа пока нет, это и есть следующая задача.