Архітектура
Чому один привілейований контейнер вміщує цілу PaaS, як її частини знаходять одна одну й які рішення визначили її вигляд. Читайте це, коли експлуатуєте платформу або хочете знати, що відбувається за кнопкою.
Один контейнер, багато демонів#
На хості працює один привілейований контейнер docker:dind — dind-hosting, чиї каталоги даних змонтовано з хоста (bind-mount). Усередині нього приватна мережа platform-net (172.28.0.0/16) містить:
| Контейнер | Роль | Адреса |
|---|---|---|
management |
служба FastAPI: консоль (SPA), REST API, MCP-сервер, узгоджувач (reconciler), воркери | 172.28.0.2 · :8900 |
traefik |
балансувальник навантаження, TLS, ACME | 172.28.0.3 · :80 :443 |
platform-pg |
стан, аудит, метрики (PostgreSQL 16) | 172.28.0.4 |
minio |
об'єктне сховище S3: резервні копії, журнали, бакети користувачів | 172.28.0.5 · :9000 :9001 |
db |
спільний демон баз даних (ще один docker:dind) |
172.28.0.10 |
projects-<alias> |
по одному демону застосунків на користувача | 172.28.0.64+ |
db-<alias> |
необов'язковий окремий демон баз даних | 172.28.1.64+ |
Чому все в одному контейнері? Один об'єкт на рівні хоста для обслуговування (один контейнер, один файл Compose, один набір bind-mount-ів, одна адреса), а контейнер management і всі дочірні демони опиняються на одному рівні в одній мережі. Слід на хості — рівно одна адреса в одній мережі-мосту, без опублікованих портів.
Зв'язок з кожним демоном: каталог сокетів#
Кожен демон — головний, db, кожен projects-<alias> — створює свій API-сокет в одному спільному каталозі (/platform-data/sockets, змонтованому з хоста). Management монтує цей каталог і спілкується з усіма демонами через той самий клієнт unix-сокетів. Він ніколи не використовує TCP-порт, а головний демон узагалі не має TCP-слухача: неавтентифікований Docker API не повинен бути досяжним із мережі hosting.
| Демон | Сокет |
|---|---|
| головний | main.sock (а також /var/run/docker.sock для docker exec dind-hosting … із хоста) |
| спільний демон баз даних | db.sock |
| застосунки користувачів | projects-<alias>.sock |
| бази даних користувачів | db-<alias>.sock |
Мережа: режим host усередині демона#
- Застосунок слухає
0.0.0.0:<port>на адресі свого демона. Traefik спрямовуєHost(blog.alex…)прямо наhttp://172.28.0.64:10000. - Доступ між демонами — звичайний мережевий рівень L3: застосунок звертається до своєї бази даних за адресою демона баз даних, а до MinIO — за
172.28.0.5:9000. Платформа передає ці адреси як змінні середовища. - Унікальність портів потрібна лише в межах одного демона, тому розподільники портів працюють окремо для кожного демона, спираються на базу даних і ніколи не використовують номери повторно протягом життя демона (без конфліктів із застарілими з'єднаннями).
Management: запуск і узгодження#
- Секретизгенерувати відсутні в
/secrets - База данихпідключитися до platform-pg, виконати міграції Alembic
- Узгодженнямережа, контейнери платформи, демон кожного користувача
- Traefikперебудувати зі збережених налаштувань ACME, перестворити за розбіжності
- Воркеризбирач метрик, перевірка доменів (watchdog), очищення резервних копій
- Прийом запитіввідкрити API, консоль і MCP
Узгоджувач створює те, чого бракує, і запускає те, що зупинено; він ніколи не знищує дані користувачів. Лише контейнер Traefik без стану перестворюється, коли змінюється його бажана специфікація — специфікація хешується в мітку й порівнюється під час кожного запуску.
Потік розгортання докладно#
- Отримання —
git clone --depth 1 [--branch ref]або розпакований архів (із захистом від обходу каталогів і перевищення розміру) у/deploys/build/<user>/<project>/<version>/. - Маніфест — розібрати
platform.yaml(суворо), визначити середовище виконання, обрати версію (маніфест → підказка → типова). - Збірка — згенерувати Dockerfile, передати контекст потоком через сокет демона (його ніколи не монтують у дочірній демон, тож вихідний код ніколи не перетинає межі між користувачами) і зібрати
platform/<slug>:<version>-<sha8>та:latest. - Запуск — зупинити й видалити старий контейнер; запустити новий з
--network host, виділеним портом, складеними змінними середовища, томами й лімітами,restart=unless-stopped. - Запис — статус, маніфест і повний журнал (у MinIO), рядок аудиту; маршрут Traefik записується після перевірки.
Конвеєр — це фонове завдання в тому самому процесі. Якщо management перезапуститься посеред збірки, перерване розгортання буде позначено як failed (повідомлення interrupted by management restart), і вам достатньо просто розгорнути знову.
Модель безпеки коротко#
| Область | Механізм |
|---|---|
| Відкритість хоста | одна адреса в мережі-мосту; жодних опублікованих портів; жодного TCP Docker API на головному демоні |
| Автентифікація | хеші паролів argon2id; JWT (8 год) і довгоживучі токени dt_…, що зберігаються як хеші |
| Авторизація | admin / user; власника перевіряють на рівні сервісів; чужі об'єкти відповідають не знайдено |
| Ізоляція користувачів | окремий демон застосунків для кожного користувача; вихідний код передається потоком під час кожного розгортання; окремі контейнери баз даних з власними обліковими даними |
| Секрети | генеруються в /secrets (0600); збережені облікові дані шифруються ключем Fernet; секретні змінні — лише для запису |
| Аудит | audit_log для кожної зміни, query_log для кожного оператора SQL-консолі |
| TLS | ACME HTTP-01 (або локальний dev-CA) після перевірки, яка доводить, що ім'я веде назад на платформу |
Спільний демон баз даних — свідомий компроміс: бази даних ізольовано межами контейнера та окремими обліковими даними для кожної бази, а не окремим демоном. Окремий демон db-<alias> прибирає цю спільність для користувачів, яким це потрібно.
Модель даних#
| Таблиця | Вміст |
|---|---|
users, api_tokens |
облікові записи, ролі, псевдоніми; хеші токенів |
dind_instances, port_allocations |
кожен демон (тип, адреса, сокет) і кожен виділений порт |
projects, project_domains |
проєкти та їхні домени зі статусами перевірки й сертифіката |
deployments, apps |
кожне розгортання з визначеним маніфестом; поточний застосунок проєкту |
project_env_vars |
змінні проєкту (секрети як токени Fernet) |
databases |
кожна база даних: СУБД, образ, демон, порт, зашифрований пароль, том |
backups |
рядки резервних копій із ключем MinIO, розміром і sha256 |
metrics_samples, metrics_hourly |
15-секундні вибірки та погодинні агрегати |
audit_log, query_log |
журнал аудиту |
upload_tokens, settings |
одноразові посилання для завантаження; налаштування всієї платформи |
Рішення, що сформували платформу#
| Рішення | Вибір і причина |
|---|---|
| Один головний DinD, що вміщує все | єдина одиниця й адреса на рівні хоста; тривіальне резервне копіювання data/ (ADR-001) |
| Unix-сокети в спільному каталозі | один уніфікований клієнт; ніде немає досяжного TCP Docker API (ADR-002, ADR-040) |
| Корінь даних кожного демона — bind-mount | overlay2 на ext4 під вкладеними демонами працює надійно (ADR-003) |
| Мережа host усередині демонів | застосунок працює на адресі демона; без внутрішнього відображення портів (ADR-006) |
| Перевірка перед сертифікатами | ніколи не просити в CA сертифікат для імені, яке не веде назад до нас (ADR-009) |
| Контексти збірки передаються потоком, а не монтуються | вихідний код ніколи не перетинає межі між користувачами (ADR-011) |
| Один вебзастосунок на проєкт | «проєкт = одна одиниця розгортання» на зразок Vercel (ADR-013) |
| Режим dev TLS поряд з ACME | повні наскрізні тести TLS без публічного DNS (ADR-014) |
| Користувачами керує адміністратор | без самореєстрації у v1 (ADR-017) |
| Змінними керує платформа | зашифровані секрети лише для запису; перестворення перебудовує середовище (ADR-045) |
| Одноразові посилання для завантаження | великі архіви безпечно обходять ліміт MCP у 4 МіБ (ADR-046) |
| Версії середовища виконання в маніфесті | короткі версії йдуть за найновішим релізом; зафіксовані версії не змінюються (ADR-047) |
| База даних зберігає свій образ | жодного випадкового мажорного оновлення каталогу даних (ADR-049) |
Чого немає в цій версії#
Розгортання на кількох хостах або в Kubernetes · горизонтальне масштабування служби management · ізоляція на рівні проєкту (замість користувача) · спільний для демонів реєстр · інтерфейс відкоту, окрім повторного розгортання старішого вихідного коду · SSO і самореєстрація · прямі зовнішні порти баз даних · резервні копії за розкладом · експортери Prometheus і сповіщення.