Skip to content

📱 Управление приложениями (демон) ​

📌 Описание ​

Ядро демона: единый интерфейс управления приложениями трёх типов — 🐳 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. Capability app.repull. AppService.RepullAppStream (DMN-132) — тот же repull живой лентой: строки скачивания слоёв образа, затем тот же RepullAppResponse, — чтобы платформа вела repull как фоновую задачу с логом; отключившийся клиент его не отменяет. Capability app.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, REST uuid), отсутствует, если не задан.

🐳 Контейнеры хоста (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/prune Engine. У контейнера, созданного не через 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-сокета — не через docker CLI; путь сокета настраивается — [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.

Распространяется по лицензии MIT.