Self‑hosted PaaS в 20 МБ памяти: что мы выкинули из классической архитектуры и почему

Мы делаем Pier — open‑source панель для деплоя приложений на свои серверы, по смыслу близкую к Coolify, Dokku или CapRover. Подключаешь Git‑репозиторий, получаешь контейнер за Traefik с сертификатом Let's Encrypt, бэкапы базы в S3 и алерты.
Функционально таких проектов много, поэтому в этой статье речь пойдёт не о том, что Pier умеет, а о том, как он устроен: мы с самого начала ставили жёсткое ограничение по ресурсам и почти каждое архитектурное решение выросло из него. За семь месяцев и ~550 коммитов набралось достаточно решений и граблей, о которых хочется рассказать.
Ограничение, с которого всё началось
Coolify после установки поднимает Laravel‑приложение, PostgreSQL, Redis, Horizon, Soketi и Traefik. В простое это 750 МБ — 1,2 ГБ RAM. Для VPS с 1 ГБ памяти это неприемлемо: панель съедает сервер раньше, чем на нём появится первое приложение.
Отсюда требование: панель в простое должна занимать десятки мегабайт, а не сотни. В итоге получилось 20–40 МБ RSS для процесса Pier плюс Traefik. Ниже о том, за счёт чего.
Стек
Слой | Выбор | Почему |
|---|---|---|
Язык | Rust | один статический бинарник, без рантайма, предсказуемая память |
HTTP | Axum + Tokio | async, WebSocket для логов и метрик |
Docker | Bollard | Docker Engine API напрямую, без вызова CLI |
Хранилище | SQLite (rusqlite) | без отдельного контейнера с СУБД |
UI | MiniJinja + HTMX + Alpine.js | серверный рендеринг, ~30 КБ JS |
Стили | Tailwind v3, собирается заранее | итоговый CSS вшит в бинарник |
Прокси | Traefik | маршрутизация и ACME без нашего кода |
Почему SQLite, а не Postgres
Панель хранит конфигурацию: проекты, сервисы, домены, секреты, историю деплоев, метрики алертов. Это небольшой объём данных с редкими записями от единственного процесса. Отдельный Postgres здесь дал бы +100–200 МБ памяти, ещё один контейнер, который нужно бэкапить и обновлять, и сетевую зависимость при старте панели.
Миграции у нас — append‑only массив SQL‑строк в коде; номер последней применённой хранится в БД. Никаких внешних инструментов миграций: бинарник при старте сам доводит схему до актуальной. Правило одно: старые миграции никогда не редактируются, только добавляются новые.
Почему HTMX, а не React
Панель управления — это в основном формы, таблицы и логи. SPA здесь означал бы отдельную сборку фронтенда, дублирование валидации и сотни килобайт JS. С HTMX сервер отдаёт готовые HTML‑фрагменты, а Alpine.js закрывает локальную интерактивность (модалки, вкладки, тумблеры).
Шаблоны MiniJinja, CSS, шрифты и переводы вшиты в бинарник через rust-embed. Следствие, о котором мы узнали не сразу: даже в debug‑сборке правка шаблона требует пересборки, потому что файлы с диска не читаются. Зато деплой самой панели — это замена одного файла.
Локализация без runtime‑оверхеда
Интерфейс переводится через rust-i18n, а текущая локаль запроса хранится в task‑local переменной Tokio. В шаблонах доступны функции t() и te(), в ошибках API — те же ключи. Каталог сейчас около 2000 ключей; добавить язык — значит положить ещё один YAML‑файл.
Аутентификация: почему мы отказались от JWT
Изначально сессии были на JWT. Потом мы перешли на opaque‑сессии в SQLite и не жалеем:
JWT нельзя отозвать без чёрного списка, а чёрный список — это и есть серверное хранилище сессий, только неудобное.
Для панели с одним инстансом stateless‑аутентификация ничего не даёт: проверка сессии — это поиск по индексу в локальной SQLite.
С серверными сессиями просто сделать то, что нужно админке: выход со всех устройств при смене пароля, ротацию ID сессии после логина, абсолютный TTL, привязку к User‑Agent, сборку мусора просроченных сессий, мгновенный оффбординг удалённого пользователя.
Сверху — TOTP 2FA, роли с проверкой прав на уровне ресурса и API‑токены для CI.
Несколько серверов
На дополнительные серверы ставится pier-agent — отдельный лёгкий бинарник, который выполняет Docker‑операции и отдаёт метрики. Центральная панель общается с агентами по HTTP, агент шлёт heartbeat.
Самая интересная часть — кросс‑серверные кластеры баз данных: реплика MongoDB или Redis, разнесённая по разным машинам. Здесь пришлось решать связность контейнеров между хостами без оверлейной сети Docker Swarm. Мы публикуем порты узлов кластера в выделенном диапазоне и открываем их в файрволе только для внутренних подсетей.
Самообновление без SSH
Панель работает не от root. Но обновление затрагивает systemd‑юниты, вспомогательные сервисы и сам бинарник — это root‑операции. Решение:
Панель пишет файл‑запрос в специальную директорию.
systemd
.path‑юнит видит файл и запускает root‑сервисpier-updater.Апдейтер скачивает последний официальный релиз, проверяет sha256 и запускает тот же установщик, что и
curl | bash.Статус и лог пишутся в файлы, которые панель показывает пользователю.
Принципиальный момент: панель может только попросить обновиться. Какую версию и откуда ставить, решает root‑сторона. Скомпрометированная панель не может подсунуть апдейтеру свой бинарник.
Сборка без Dockerfile
Для репозиториев без Dockerfile мы интегрировали Railpack (преемник Nixpacks от Railway) поверх локального BuildKit. Это единственная по‑настоящему тяжёлая часть системы: компиляция пользовательского кода требует 4+ ГБ RAM и десятки гигабайт под кэш. Поэтому:
на хостах с RAM < 4 ГБ UI показывает жёсткое предупреждение;
параллельные сборки ограничены (по умолчанию одна);
кэш BuildKit ежедневно подрезается до ~10 ГБ / 7 дней;
установку Railpack можно отключить флагом, тогда остаются деплой из Dockerfile, образа и Compose.
Грабли
Хостеры фильтруют порты. На одном из VPS входящие соединения на 5432 молча отбрасывались на уровне сети хостера. ufw тут ни при чём — к тому же он не управляет портами, которые публикует Docker. Теперь для публичных портов БД мы советуем нестандартные значения.
Telegram‑алерты из России. api.telegram.org на российских VPS часто недоступен. Уведомления в Telegram требуют прокси или туннеля через зарубежный сервер, поэтому у нас есть и другие каналы: email, Discord, Slack.
Traefik, оставленный выключенным. Одно действие в панели могло остановить контейнер прокси и не поднять его обратно — все сайты на сервере ложились. Теперь панель гарантирует, что Traefik не может остаться в остановленном состоянии.
Один публичный порт, привязанный дважды. При редактировании сервиса порт мог попасть в конфигурацию контейнера дважды, и Docker отказывался его запускать. Мелочь, но из таких мелочей и состоит большая часть работы над PaaS.
Bitnami и MongoDB на новых ядрах. Обновление образов сломало кластерные шаблоны, а MongoDB не стартовала на свежих ядрах Linux. Поддержка каталога из 40 шаблонов оказалась отдельной постоянной работой.
Итоги
Coolify | Pier | |
|---|---|---|
RAM в простое | 750 МБ — 1,2 ГБ | 20–40 МБ (+ Traefik) |
Диск | ~1 ГБ | 15–30 МБ |
Процессов/контейнеров панели | 6+ | 1 + Traefik |
Минимальный VPS | 2 ГБ / 2 vCPU | 512 МБ / 1 vCPU |
Лицензия AGPL-3.0. Установка: curl -fsSL https://pier.team/install | sudo bash.
Код: github.com/joveptesg/pier
Документация
Если у вас есть опыт с Coolify, Dokku или CapRover, расскажите в комментариях, чего вам в них не хватало. Нам это поможет решить, что делать дальше.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.