DinD HostingПосібник
ENУК
Огляд/Посібник/Керування платформою
РОЗДІЛ 16

Архітектура

Чому один привілейований контейнер вміщує цілу 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+
зовні Браузери*.alex.example.com REST-клієнти · CIBearer JWT / токен dt_ ШІ-агентиClaude Code · MCP Ingress хоста проксі абоnftables на хостіваше рішення → 10.10.10.10 dind-hostingголовний DinD · privileged · міст «hosting» · 10.10.10.10 · портів не публікує platform-net · 172.28.0.0/16 · статичні IP Traefik:80 :443 · ACME172.28.0.3 management:8900 UI·API·MCP172.28.0.2 platform-pgстан · метрики172.28.0.4 MinIOS3 · копії172.28.0.5 projects-alex172.28.0.64 blogstatic:10000 shopnode 24:10001 dbспільний демон БД172.28.0.10 postgresshop-main:20000 mysqlblog-wp:20001 projects-bob172.28.0.65 apipython 3.12:10000 sitephp 8.5:10001 db-aliceнеобов'язково · окремий postgresanalytics:20000 /platform-data/sockets/ main.sock · db.sock · projects-alex.sock · … data/ на хості монтується в головний DinD: main-docker · platform/{db,minio,deploys, traefik,secrets,sockets} · dind/ умовні позначення HTTP-маршрут за Host Docker API через unix-сокети демон користувача (застосунки) демон баз даних єдиний контейнер рівня хоста
Повна картина: один контейнер на рівні хоста, одна мережа платформи і власний демон для кожного користувача.

Чому все в одному контейнері? Один об'єкт на рівні хоста для обслуговування (один контейнер, один файл 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 усередині демона#

Traefik172.28.0.3 · :80 :443маршрут: Host(blog…)projects-alexмережевий простір = 172.28.0.64app-blog0.0.0.0:10000app-shop0.0.0.0:10001Контейнери працюють із --network host, що в демоніозначає власну мережу контейнера демона:без публікації портів і NAT; порти унікальні в межах демона.172.28.0.64:10000демон БД172.28.0.10postgres :20000 · mysql :20001MinIO172.28.0.5S3 :9000 · console :9001DB_MAIN_HOST/PORTS3_ENDPOINTКожен довгоживучий контейнер має статичну адресу в platform-net (172.28.0.0/16); демони користувачів — 172.28.0.64–127, демони БД — 172.28.1.64–127.Порти застосунків — 10000–19999 на демон, порти баз даних — 20000–29999 на демон БД; лічильник не повторює номер упродовж життя демона.
Робочі навантаження користувачів працюють із мережею host усередині власного демона, тож кожен застосунок слухає безпосередньо на стабільній адресі.
  • Застосунок слухає 0.0.0.0:<port> на адресі свого демона. Traefik спрямовує Host(blog.alex…) прямо на http://172.28.0.64:10000.
  • Доступ між демонами — звичайний мережевий рівень L3: застосунок звертається до своєї бази даних за адресою демона баз даних, а до MinIO — за 172.28.0.5:9000. Платформа передає ці адреси як змінні середовища.
  • Унікальність портів потрібна лише в межах одного демона, тому розподільники портів працюють окремо для кожного демона, спираються на базу даних і ніколи не використовують номери повторно протягом життя демона (без конфліктів із застарілими з'єднаннями).

Management: запуск і узгодження#

  1. Секретизгенерувати відсутні в /secrets
  2. База данихпідключитися до platform-pg, виконати міграції Alembic
  3. Узгодженнямережа, контейнери платформи, демон кожного користувача
  4. Traefikперебудувати зі збережених налаштувань ACME, перестворити за розбіжності
  5. Воркеризбирач метрик, перевірка доменів (watchdog), очищення резервних копій
  6. Прийом запитіввідкрити API, консоль і MCP

Узгоджувач створює те, чого бракує, і запускає те, що зупинено; він ніколи не знищує дані користувачів. Лише контейнер Traefik без стану перестворюється, коли змінюється його бажана специфікація — специфікація хешується в мітку й порівнюється під час кожного запуску.

Потік розгортання докладно#

  1. Отримання — git clone --depth 1 [--branch ref] або розпакований архів (із захистом від обходу каталогів і перевищення розміру) у /deploys/build/<user>/<project>/<version>/.
  2. Маніфест — розібрати platform.yaml (суворо), визначити середовище виконання, обрати версію (маніфест → підказка → типова).
  3. Збірка — згенерувати Dockerfile, передати контекст потоком через сокет демона (його ніколи не монтують у дочірній демон, тож вихідний код ніколи не перетинає межі між користувачами) і зібрати platform/<slug>:<version>-<sha8> та :latest.
  4. Запуск — зупинити й видалити старий контейнер; запустити новий з --network host, виділеним портом, складеними змінними середовища, томами й лімітами, restart=unless-stopped.
  5. Запис — статус, маніфест і повний журнал (у 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 і сповіщення.