📱 Управление приложениями (демон)
📌 Описание
Ядро демона: единый интерфейс управления приложениями трёх типов — 🐳 Docker-контейнеры, 📦 нативные приложения (systemd-юниты) и 🔧 обычные процессы (PID). Ключевое отличие от Portainer/Coolify: не только Docker.
🎯 Сценарии использования
asc app install helloworld— установка из реестра (Docker или нативное — решает манифест).asc app start|stop|restart|status|logs <name>— управление жизненным циклом.startсразу подключается к консоли приложения (Docker-приложения, интерактивный терминал — какdocker runбез-d);asc app start -d <name>запускает в фоне, без подключения. Если хосту не хватает ресурсов, заявленных вrequirementsманифеста,startпредупреждает с цифрами и спрашивает, продолжать ли на свой страх и риск (DMN-029).- Пользовательские названия (DMN-024):
asc installспрашивает название приложения (Enter — по умолчанию; неинтерактивно —--name).asc app listпоказывает и оригинальный ID, и NAME, а все команды принимают и то и другое:asc app start "My Server"=asc app start cs2-server. При совпадении названия с чужим id приоритет у id; неоднозначное название (несколько одинаковых) — ошибка с подсказкой использовать id. И id, и название сравниваются без учёта регистра (DMN-059) — id канонически в нижнем регистре (HOMEBARставится какhomebar, см. 📦 package-manager), поэтомуasc app start HOMEBARдо него доходит; точное написание по-прежнему приоритетнее приведённого к регистру, так что два названия, отличающиеся только регистром, остаются адресуемыми. - Несколько инстансов одного приложения (DMN-033): повторная установка уже установленного пакета больше не завершается ошибкой — создаётся новый инстанс
<пакет>-2,<пакет>-3, … (он же становится названием по умолчанию), поэтомуasc install nginxдважды даёт два независимых приложенияnginx/nginx-2. Повторная установка целого стека называет новые инстансы так же;--name <префикс>при установке целого стека называет по этому же принципу каждое приложение-участник (DMN-034), так как у стека нет собственной сущности. asc app disk <имя>(DMN-035): дисковое пространство — прогресс-бар против квоты, если заданquota.max_disk, затем разбивка по Docker-образу, репозиторию, приватным данным и кастомным томам (именованные тома Docker помечены как общие и не входят в сумму по каталогу приложения).- Версия образа и repull (DMN-120):
AppService.GetAppImageсообщает, на каком образе работает Docker-приложение — ссылка, тег, версия (лейблorg.opencontainers.image.version, иначе тег, если он неlatest), локальный дайджест манифеста изRepoDigestsи id образа, из которого создан контейнер, — а сcheck_remoteещё и дайджест, который реестр отдаёт для тега сейчас (/distribution/{name}/jsonчерез Engine: запрос манифеста без слоёв, таймаут 15 с, те же креды реестра, что при установке). Вердикт:RESTART_REQUIRED, если тег уже указывает на более новый локальный образ, чем у контейнера, иначеUP_TO_DATE/UPDATE_AVAILABLEпо дайджесту, иначеUNKNOWN(локальная сборка, образ не скачан или реестр не ответил).AppService.RepullAppзаново скачивает образ из реестра с тегомlatest(или без тега) и перезапускает запущенное приложение, если образ изменился; закреплённый тег, дайджест, локальная сборка или не-Docker приложение отвергаются сFAILED_PRECONDITION. Отдельного пути пересоздания для рестарта не нужно: drift-проверка настроек (pkg::refresh) теперь сравнивает id образа контейнера с id, на который указывает ссылка, поэтому любой старт/рестарт после pull — в том числе после ручногоdocker pull— пересоздаёт контейнер на новом образе, какdocker compose up. Capabilityapp.repull.AppService.RepullAppStream(DMN-132) — тот же repull живой лентой: строки скачивания слоёв образа, затем тот жеRepullAppResponse, — чтобы платформа вела repull как фоновую задачу с логом; отключившийся клиент его не отменяет. Capabilityapp.repull.stream. asc app clone <имя> [--name <имя-клона>](DMN-019): полная копия инстанса приложения — репозиторий, config и data — под новым id (та же нумерация<id>-N, что и при повторномasc install, DMN-033), с живым прогресс-баром копирования в терминале. Сам рантайм не копируется (Docker-контейнер/systemd-юнит/процесс скопировать нельзя) — он пересоздаётся заново из скопированного манифеста и настроек, точно как при установке. Клон всегда стартует остановленным.AppService.CloneApp/CloneAppStream(DMN-113) отдают ту же операцию по gRPC/REST (POST /v1/apps/{id}/clone) для кнопки «Клонировать» на платформе; потоковый вариант репортит прогресс копирования текстовыми строками (по одной на каждый целый процент) — точно какInstallAppStream/UpgradeAppStream. Клонирование на другую ноду (NODE-045) переиспользует это локально, на целевой ноде, как фазу «установка» межнодового переноса — см. 🧬 app-cloning. Этому переносу нужно заново собрать спецификацию установки из собственных метаданных приложения — точно так же, как это уже делает внутри себяasc app upgrade(meta.source/meta.package/meta.repo_path/meta.branch), поэтомуApp(ListApps/GetApp) теперь несёт ещёpackage,repo_pathиbranchрядом с уже существующимиsource/version/commit— все опциональные, все отсутствуют у приложения, установленного до DMN-113.asc app list— пользователь видит только свои приложения;sudo asc app list— приложения всех пользователей. Короткие алиасы (DMN-025):asc lsиasc ps— тот же вывод и те же права.asc ports [<имя>]/asc app ports [<имя>](DMN-049): опубликованные порты приложения с транспортом (27015/tcp,27015/udpили27015/tcp+udp, когда оба на одном порту), взятые из настроекtype: ports— поэтому остановленное приложение всё равно показывает порты, которые оно займёт при следующем старте. Пакет, который фиксирует свою контейнерную сторону (container:, DMN-052), показывается как8080->3000/tcp: сначала хостовый порт, затем контейнерный за ним. Без имени — таблица всех видимых приложений с их портами (root видит приложения всех пользователей).AppService.ListAppPorts(DMN-079) — тот же список по gRPC, для платформы: docker-приложения возвращают свои порты, остальные виды — пустой список.asc stats [--live] [--sort cpu|mem]— потребление CPU, памяти, диска и сети по приложениям (аналогdocker stats, см. 📊 monitoring).- Подкоманды списка (DMN-049):
asc ls ports,asc ls diskиasc ls statsпереключают тот же список приложений на вид портов, использования диска или живой статистики — зеркалаasc ports/asc disk/asc stats. asc stacks(DMN-051): установленные приложения, сгруппированные по стеку (asc.stack.yaml), из которого они пришли, деревом — имя стека с пометкой, сколько его приложений сейчас запущено ([running/total]), затем его приложения ветками├──/└──(id, name, kind, state, version, uuid — те же колонки, что и вasc ls), root видит приложения всех пользователей. Стек определяется поmeta.package("<стек>/<приложение>", записывается при установке — см. 📦 package-manager); у приложения, установленного самого по себе, вpackageнет/, и оно сюда не попадает.- Платформа выполняет те же операции через API демона.
- После ребута сервера демон восстанавливает состояние приложений (running/stopped).
🏗️ Техническое решение
👥 Группы приложений по пользователям
- Каждое приложение принадлежит Linux-пользователю, который его установил (владелец фиксируется в
meta.jsonи индексе). - Обычный пользователь видит и управляет только своей группой приложений.
- Через
sudo(или отroot) видны и доступны приложения всех пользователей — вывод группируется по владельцам. - API демона применяет то же правило: контекст запроса определяет видимую группу.
- При работающем системном демоне (DMN-042) команды жизненного цикла (
ls/status/install/app start|stop|restart|logs|remove|info) идут через его unix-сокет/run/asc/asc.sock: демон берёт uid вызывающего у ядра (SO_PEERCRED) и применяет это правило к общему системному стору — обычный пользователь управляет своими приложениями в/asc/appsбез sudo и без группы docker, аasc lsиsudo asc lsнаконец показывают одно и то же. Подробнее — 📡 api. - Отчётные команды идут тем же маршрутом (DMN-053):
asc disk,asc ports,asc stats,asc stacksиasc upgradeтоже ходят через сокет. Раньше они читали in-process своё дерево приложений вызывающего, поэтому пользователь, чьи приложения лежат в системном дереве, видел их вasc lsи «Приложения не установлены» во всех отчётах рядом — аasc app upgrade <id>отвечал «приложение не найдено» для приложения, которое очевидно есть. In-process осталисьasc app cloneиasc backup: им по-прежнему нужен прямой доступ к системному дереву (root или отсутствие демона).
📂 Хранилище приложений: /asc/apps/
Каждое приложение живёт в каталоге по своему ID:
/asc/apps/<id>/
├── config/ # ⚙️ настройки приложения (см. asc.settings.yaml в package-manager.md)
├── repository/ # 📦 клонированный репозиторий приложения (версии — git-теги)
├── data/ # 💾 volumes — если приложение в Docker
└── meta.json # 📇 информация о приложении: id, uuid, имя, пользовательское название, владелец, версия (тег), источник, отслеживаемая ветка, состояние- Установка = клонирование репозитория пакета в
repository/; переключение версии — checkout нужного git-тега (подробнее — 📦 package-manager). meta.json— источник правды для восстановления индекса после сбоя/ребута.- Разделение путей по пользователям:
/asc/apps/(вместе с/etc/asc/config.tomlи/var/lib/asc) — дерево root-установки: системный демон иsudo asc. Запускascот обычного пользователя без работающего системного демона работает со своим приватным деревом в~/.asc/:~/.asc/apps,~/.asc/data,~/.asc/config.toml— так пользователь редактирует настройки своих приложений и конфиг без sudo. При наличии демона команды жизненного цикла работают с общим системным деревом через сокет демона (DMN-042, см. выше). Root-секция[policy]по-прежнему читается из системного конфига и не переопределяется на уровне пользователя.
🆔 UUID инстанса (DMN-044)
Кроме id, каждый инстанс получает UUID, который генерируется при asc install и хранится в meta.json в поле uuid. Он переживает upgrade и выводится последней колонкой asc ls:
ID NAME KIND STATE VERSION UUID
pingpong Ping Pong docker stopped 0.1.0 6f8a1c2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b
legacy-app Legacy App docker stopped 1.2.0 -Зачем второй идентификатор: id переиспользуется. После удаления helloworld-2 этот id освобождается для следующей установки (DMN-033) — и всё, что живёт дольше приложения (сохранённые учётные данные (DMN-045), записи платформы, история аудита), молча привязалось бы к другому приложению. UUID уходит вместе с инстансом и никогда не выдаётся повторно.
- Генерируется как случайный UUIDv4 (RFC 4122) из
/dev/urandom— ради шестнадцати байт не тянем отдельную зависимость. - Необязателен в
meta.json: у приложений, установленных до DMN-044, ключаuuidнет — они читаются как обычно и показывают-. Ничего не дописывается втихую. asc app cloneвыдаёт клону свой UUID — это отдельный инстанс, а не копия идентичности.- В API отдаётся как
App.uuid(поле proto 10, RESTuuid), отсутствует, если не задан.
🐳 Контейнеры хоста (DMN-102/DMN-112)
asc docker ps показывает все контейнеры хоста, а не только установленные через ASC, а asc docker stats — их живые CPU и память. Обе команды идут через Engine API, как и весь драйвер, и обе доступны только root: у контейнера, созданного не через ASC, нет владельца, по которому работает проверка прав, — значит, не-root вызывающего не к чему привязать.
- Контейнер, который является приложением ASC, помечается как таковой. Связь выводится из runtime в
meta.jsonсамого приложения, а не поиском префиксаasc-в имени контейнера: этот префикс — соглашение об именовании, которого придерживается демон, а не заявка на владение, и оператор волен назвать так же контейнер, поднятый руками. - Лейблы Compose
com.docker.compose.project/.serviceподнимаются из общей карты лейблов в отдельные поля, чтобы вызывающему не приходилось знать, как именно Compose пишет свои ключи. - Размеры запрашиваются явно (
asc docker ps -s,size=1): чтобы ответить, Engine обходит writable-слой каждого контейнера — на загруженном хосте это заметная пауза и лишняя работа для того, кому нужен просто список. - Статистика — отдельный вызов, а не колонка списка. Процент CPU требует двух замеров с интервалом ~500 мс, и список не должен платить эту цену при каждом открытии — ровно то же разделение уже есть у
asc lsиasc stats. Оба замера делаются вокруг одного общего сна, поэтому пятьдесят контейнеров стоят тех же ~500 мс, что и один, а фильтр по id применяется до окна, а не после. - Контейнер, исчезнувший между двумя замерами, просто отсутствует в результате: это штатная гонка, а не ошибка вызова.
Управления жизненным циклом здесь намеренно нет. Контейнер приложения ASC нельзя запускать и останавливать мимо менеджера приложений — desired_state сверяется после перезапуска демона, контейнер вернётся сам, и оператор прочтёт это как баг; для этого есть asc app start|stop. У чужого контейнера вообще нет модели прав — это отдельная задача со своей capability.
🧹 Инвентарь хоста и очистка (DMN-104/DMN-105)
asc docker images|volumes|networks|df расширяют инвентарь хоста за пределы контейнеров: все образы, именованные тома и сети, которые знает Engine, плюс сводка df по образцу docker system df. asc docker prune images|volumes|build-cache [--dry-run] [--dangling] удаляет неиспользуемое — и никогда не трогает то, что всё ещё нужно установленному приложению.
- Образ или том, который объявляет установленное приложение, защищён — запущено оно или нет. Защищённое множество считается по манифесту и настройкам каждого приложения (образ, к которому оно сейчас разрешается через
runtime.image/runtime.image-build, и каждый именованный том из настройкиtype: volumes), а не по лейблам или соглашению об именах.asc docker images/volumesпомечают защищённую строку и называют приложение, которое её держит. - Удаление — поштучное, никогда не через собственный массовый
/images/pruneили/volumes/pruneEngine. У контейнера, созданного не через ASC, нет лейбла-владельца, поэтому массовая очистка с фильтром либо ничего не отфильтрует (без фильтра) и рискует гонкой удалить что-то используемое, либо фильтрует по лейблу, который демон начал ставить только с DMN-105, и тогда образ любого ранее созданного контейнера остаётся без защиты. Поштучное удаление делает проверку защищённого множества точной, а--dry-run— отчётом о том, что случится на самом деле, а не оценкой. - Сети — только инвентарь,
asc docker prune networksне существует. ASC никогда не создаёт и не удаляет сети сам, а собственный prune сетей у Engine сносит любую сеть без запущенных контейнеров — включая сеть остановленного compose-стека, что молча ломает его следующийup. Показ сетей без пути удаления полностью обходит эту ловушку. asc docker dfне дешёвый и никогда не опрашивается. Как иdocker system df, ответ заставляет Engine обойти каждый слой образа и каждый том; вызывать его при открытии раздела очистки, не по таймеру.- Лейблы контейнера (
asc.managed,asc.app.id,asc.app.uuid) начиная с DMN-105 пишутся на каждый контейнер, который создаёт ASC, — на будущее, для вызывающего, которому нужно опознать ASC-контейнер без сравнения имени. Они чисто информационные: проверка дрейфа настроек (container_applied) лейблы никогда не читает, поэтому их появление не может вызвать нежелательное пересоздание уже работающего контейнера.
⚙️ Ядро
- Драйверы: трейт
AppDriver { start, stop, restart, state, logs, remove }с реализациямиDockerDriver(через Docker Engine API поверх unix-сокета — не черезdockerCLI; путь сокета настраивается —[docker] socket, по умолчанию/var/run/docker.sock),SystemdDriver(юнитыasc-app-<id>.service),ProcessDriver(supervised PID: pid-файл и лог-файл в каталоге приложения). Установка приложения (создание контейнера/юнита из манифеста) — зона пакетного менеджера (DMN-003). - Docker Engine API: демон общается с Docker через Engine API (клиент
bollard, unix-сокет), а не через CLI — это работает и для rootless-инсталляций, и при нестандартном расположении сокета (достаточно указать путь в конфиге). Управляющие операции (start/stop/inspect/create/remove) — синхронные; стрим логов и attach для консоли — асинхронные по тому же сокету. Создание контейнера само скачивает образ, если его нет на хосте (pull через тот же Engine API); ответ Engine с любым HTTP-статусом не считается недоступностью Docker — пользователю показывается сообщение самого Engine. - Индекс приложений:
meta.json— источник правды; в MVP индекс строится сканированием/asc/apps/*/meta.jsonпри обращении, при старте демон сверяет желаемое состояние (desired_state) с реальностью (контейнеры, юниты, процессы) и дозапускает упавшее. Локальная БД (SQLite) появится, когда добавится состояние сверх meta.json (метрики, история операций). - Логи: единый интерфейс — docker logs / journald / файл; стрим наружу через 🖥️ console.
- Cluster-мод (пост-MVP): несколько узлов, работающих вместе как платформа. Несколько инстансов одного приложения на одном узле уже работают (DMN-033/034, выше).
- CLI-команды MVP:
asc status,asc stats,asc ports,asc stacks,asc app list|install|remove|start|stop|restart|logs|info|disk|ports|clone|settings(+ алиасыasc ls/asc psдля списка иasc ls ports|disk|statsдля видов портов/диска/статистики),asc service(управление самим демоном),asc docker ps|stats|images|volumes|networks|df|prune(контейнеры, образы, тома, сети хоста и очистка).
🔗 Связанные задачи
DMN-002, DMN-004, DMN-019, DMN-044, DMN-051, DMN-053, DMN-102, DMN-104, DMN-105, DMN-112, DMN-113, FE-005, FE-108, NODE-039, BE-040, FE-110, NODE-044, BE-008, FE-024 в ROADMAP.md.