Четыре сервиса, один сервер, ноль оправданий
Платформа выросла в четыре отдельных сервиса — Mini App для торговой симуляции, бот привлечения, флот AI-агентов и админ-панель управления — у каждого свой рантайм, свой порт и своё мнение о том, как стартовать. На этапе разработки всё крутилось локально через подручные скрипты. Переход в продакшн означал превратить набор процессов «работает у меня на машине» во что-то, что стартует надёжно, держится в работе и может обновляться без падения всей платформы. У клиента был один VPS. Никакого Kubernetes, никаких managed-сервисов, никакой DevOps-команды. Только сервер и ожидание, что всё будет работать.
оркестрация и systemd
Мы поставили Docker Compose как слой оркестрации для дата-тира — PostgreSQL 16 и Redis 7 крутятся в контейнерах с персистентными томами и healthcheck. Сами прикладные сервисы работают как systemd-юниты, а не контейнеры — это был осознанный выбор: процессам агентов нужен прямой доступ к файлам сессий Telegram на диске, а админ-фронту выгодно встроенное управление процессами Next.js. У каждого сервиса — admin API, admin frontend, бот привлечения и раннер агентов — свой systemd-юнит с правильным порядком зависимостей. База данных и Redis-контейнеры должны быть здоровы до старта любого прикладного сервиса. Бот и агенты зависят от доступности admin API, потому что они делят конфигурацию, хранящуюся в PostgreSQL. Чтобы выставить эту цепочку зависимостей правильно, потребовалось несколько итераций с `After=`, `Requires=` и healthcheck-скриптами, которые реально проверяют готовность сервиса, а не просто наличие процесса.
миграции и данные
Миграции базы идут через Alembic, который занимается эволюцией схемы по мере изменения модели данных платформы. Но более интересным миграционным вызовом стал разовый переход с SQLite на PostgreSQL. Проект стартовал на SQLite на ранней разработке — быстро прототипировать, ноль конфигурации. При переезде в продакшн все существующие данные нужно было утащить с собой. Миграция SQLite → PostgreSQL — одна из тех штук, что звучат как однострочник, но им не являются. Типы данных не маппятся чисто — динамическая типизация SQLite означает, что целые, булевы и таймстемпы хранятся как то, что ORM в тот момент решил записать. Поведение auto-increment в двух базах отличается, а внешние ключи, которые SQLite молча принимал, в PostgreSQL потребовали явной обработки. Мы написали миграционный скрипт, который читает каждую таблицу SQLite, маппит колонки на типы, совместимые с PostgreSQL, обрабатывает сброс sequence для auto-increment и сохраняет связи внешних ключей. Он отработал один раз, успешно, и мы оставили его в репозитории для документации — но запускать его второй раз никто не хочет.
nginx и деплой
Nginx стоит перед всем как reverse proxy, маршрутизирующий запросы к нужному сервису по поддомену. У админ-панели, API, эндпоинта редиректа трекинговых ссылок и Mini App — каждому свой поддомен, у каждого свои SSL-сертификаты под управлением Certbot. Продление сертификатов с несколькими поддоменами потребовало подхода с wildcard-сертификатом — индивидуальное управление сертификатами для четырёх поддоменов стало бы головной болью. Конфигурация Nginx включает rate limiting на публичном эндпоинте редиректа и обработку upgrade WebSocket для live-функций админки.
Деплой-скрипт связывает всё вместе. Он подтягивает последний код, накатывает миграции Alembic, пересобирает фронт и рестартует сервисы в правильном порядке. Критическое требование — идемпотентность: запуск скрипта дважды должен давать тот же результат, что и один запуск. Никаких осиротевших процессов, никаких дублирующихся миграций, никаких полусобранных фронтов. Каждый шаг проверяет предусловия до выполнения: миграции запускаются только при наличии ожидающих ревизий, фронт пересобирается, только если исходники изменились, а рестарты сервисов используют логику systemd, которая корректно обрабатывает уже остановленные сервисы. В конце скрипт выводит сводку по healthcheck — зелёные галочки для сервисов, ответивших корректно, красные флаги для тех, что не поднялись в ожидаемый таймаут.
результаты
Результат — продакшн-окружение, в которое команда клиента деплоит одной командой и мониторит стандартными инструментами systemd. Общий простой при типовом деплое — менее 30 секунд, время на последовательный рестарт сервисов. Честное ограничение: это конфигурация на одном сервере без резервирования. Если VPS упадёт, упадёт всё. Для текущего масштаба это приемлемый компромисс — платформа обслуживает сотни пользователей, не миллионы. Но архитектура спроектирована так, что каждый сервис можно вытащить на отдельный сервер, когда придёт время. Docker Compose для дата-тира, systemd для прикладных процессов и Nginx для маршрутизации — скучные технологии, которые работают и которые кто-то кроме нас способен понять и поддерживать.