Davit: нативный macOS GUI для контейнеров Apple

Обзор

Давит (davit) — нативный macOS-интерфейс для контейнерной платформы Apple. Думайте о нём как об OrbStack или Docker Desktop, только для стека «Linux-контейнеры как лёгкие виртуальные машины», который Apple поставляет для Apple silicon.

Давит (davit) — это корабельный кран, которым поднимают грузы и шлюпки за борт. Именно это приложение в каком-то смысле и делает с вашими контейнерами.

Приложение написано полностью на SwiftUI — никакого Electron, никаких веб-вьюх. Davit напрямую подключает библиотеку Apple ContainerAPIClient и общается с container-apiserver через XPC — тем же путём, которым ходит утилита container. CLI-бинарник никогда не вызывается: списки, жизненный цикл, живая статистика, стриминг логов, загрузка образов, управление томами и сетями, интерактивная оболочка (davit exec) и даже настройка launchd-сервисов — всё идёт через API.

Возможности

  • Дашборд (Dashboard) — статус сервисов с кнопкой запуска/остановки, счётчики ресурсов, использование диска с меню очистки высвобождаемого места, живой суммарный CPU-график по запущенным контейнерам.

  • Контейнеры (Containers) — список с живыми показателями CPU/памяти/IP для каждой строки, запуск/остановка/kill/перезапуск/удаление, prune, поиск и Редактировать и пересоздать (Edit & Recreate) (контейнеры на этой платформе неизменяемы, поэтому «редактирование» открывает диалог запуска, предзаполненный портами/переменными окружения/монтированиями/ресурсами/сетью контейнера — за вычетом entrypoint/CMD/env образа, чтобы отображались только ваши настройки, — и заменяет контейнер после подтверждения). Детальное представление включает:

    • Обзор (Overview): образ, команда, платформа, ресурсы, сеть (IP/MAC/шлюз/hostname), опубликованные порты с кнопкой «Открыть в браузере», монтирования, переменные окружения, метки.

    • Логи (Logs): стриминг в режиме -f (follow), переключатель boot-лога, выбор tail, копирование.

    • Статистика (Stats): живые графики CPU %, памяти и дискового I/O (Swift Charts); плитки для CPU, памяти, занятого дискового пространства, сети и количества процессов.

    • Inspect: отформатированный сырой JSON.

    • Терминал (Terminal): открывает интерактивную оболочку в Terminal или iTerm (davit exec через XPC).

    • Файлы (Files): просмотр файловой системы контейнера — навигация, загрузка, выгрузка, удаление (через exec + копирование внутрь/наружу).

  • Образы (Images) — загрузка с живым прогрессом, Pull Latest для обновления тега любого образа до актуального дайджеста (контекстное меню или детальное представление образа), запуск из образа, тегирование, удаление, prune; варианты платформы для каждого образа, размер, дайджест, список использующих образ контейнеров и вкладка Layers (размер каждого слоя + команда, которой он был создан).

  • Тома (Volumes) — создание (с указанием размера), удаление, prune, открытие backing-образа в Finder, значки использования и Просмотр файлов (Browse Files) (монтирует том во временный вспомогательный контейнер и переиспользует браузер файлов).

  • Сети (Networks) — создание (подсеть / internal), удаление, prune, счётчик подключённых контейнеров.

  • Локальный DNS (Local DNS) — Settings → Platform → Registry & DNS создаёт DNS-домены хоста (container system dns, один запрос прав администратора): установите один как основной — и контейнеры разрешаются как name.<domain> с вашего Mac; в обзоре каждого контейнера отображается его name.<domain>.

  • Глобальный поиск (Global search) — ⌘K открывает палитру для перехода к любому контейнеру, образу, тому, сети или машине.

  • Уведомления об остановке (Stop notifications) — включается по желанию в Settings → General: macOS-уведомление при неожиданной остановке контейнера (сбой или внешняя остановка); остановки, инициированные вами, уведомлений не генерируют.

  • Машины (Machines) — машины контейнеров Apple (лёгкие виртуальные машины общего назначения с примонтированным домашним каталогом и стабильным DNS-именем .machine): создание из любого образа с настройкой CPU/памяти, загрузка/остановка, выбор основной машины, удаление; детальное представление с обзором конфигурации, стриминговыми логами, живой статистикой и inspect-JSON; одним кликом открывается Терминал (Terminal) в login-оболочку машины; редактирование CPU/памяти/монтирования домашнего каталога (применяется при следующем запуске). Headless-режим: Davit machine list|create|boot|stop|delete|exec|set.

  • Диалог запуска контейнера (Run Container sheet) — выбор образа, имя, команда, порты, переменные окружения, монтирования томов/директорий, лимиты CPU/памяти, выбор сети. Можно вводить запрос для поиска по Docker Hub или указать произвольный reference; образы только для amd64 определяются автоматически: при установленном Rosetta Davit добавляет --arch amd64 за вас (с предупреждением о производительности), без него запуск блокируется с точной командой установки для копирования. Davit run [flags] IMAGE [COMMAND…] в headless-режиме (запуск одного контейнера в стиле docker — см. Binary modes) с поддержкой -d/--rm/--pull missing|always|never и тем же набором docker-подобных флагов, что и compose.

  • Сборка образов (Build images) — Images → Build Image: выберите папку с контекстом и Dockerfile, задайте тег и build-args, и Davit запускает BuildKit-сборщик платформы (стартует его при необходимости), а затем загружает и тегирует результат в хранилище образов. Davit build -t <tag> <context-dir> в headless-режиме.

  • Импорт Compose (Compose import) — Containers → ⋯ → Import Compose File: разбирает compose-файл, показывает предпросмотр того, что будет создано (сервисы в порядке depends_on, именованные тома, сети, эквивалентная команда container run для каждого сервиса и предупреждения о неподдерживаемых опциях), затем создаёт и запускает стек — с учётом healthcheck, profiles, условий depends_on (service_healthy, service_completed_successfully), env_file:/entrypoint: и интерполяции ${VAR} из .env, расположенного рядом с файлом, объединённого с переменными окружения процесса. Значения в .env и env_file: сами интерполируются при загрузке (паритет с docker): значение в двойных кавычках или без кавычек, например VAR="${HOME}/x", раскрывается относительно переменных окружения процесса и ранее встреченных записей в тех же или предыдущих файлах; значение в одинарных кавычках (VAR='${HOME}/x') остаётся буквальным; встроенные комментарии (` # …) удаляются в стиле docker; известные отличия от compose-go (escape-последовательности вроде `\$ не обрабатываются — используйте $$; вложенный дефолт ${A:-${B}} сохраняет дефолт буквально) — намеренные. Headless-режим: полный жизненный цикл — Davit compose plan | up [-d] [--down-on-failure] | down [-v] | ps | logs [-f|--follow] [--tail <n>] | stop | start | restart | pull | exec <service> <command…>, каждая команда принимает [-f <file>] [--env-file <path>] [--profile <p>]… [--verbose|-q|--quiet] [service…] — файл обнаруживается автоматически, как в docker (compose.yaml, compose.yml, docker-compose.yml, docker-compose.yaml, с обходом родительских каталогов; побеждает COMPOSE_FILE). Флаг --down-on-failure (только для up) сносит контейнеры, созданные в рамках этого вызова, если up завершился с ошибкой на полпути, не затрагивая уже работавшие сервисы, которые он переиспользовал; --verbose/-q/--quiet управляют диагностикой для каждой команды (взаимоисключающие). Указание имён сервисов сужает область действия команды: plan/up добавляют замыкание зависимостей, а stop/start/restart/pull/ps и down применяются ровно к указанным сервисам (поведение docker). Обнаружение сервисов: DNS платформы не умеет разрешать имена compose-сервисов, поэтому up/start/restart вписывают управляемые записи в /etc/hosts (с суффиксом # davit-compose) во все запущенные контейнеры проекта, сопоставляя каждое имя сервиса и контейнера с его текущим IP — обновляется при каждом запуске, так что новый IP пересозданного сервиса становится доступен продолжающим работать контейнерам. Оговорка: контейнеры, пересозданные вне compose, хранят устаревшие записи до следующего up или start; образы без /bin/sh патчу недоступны (выводится предупреждение). Лимиты ресурсов: сервис без mem_limit/cpus (или deploy.resources.limits) получает память/CPU по умолчанию для контейнеров платформы — настраивается в Settings → Platform; поскольку каждый сервис — это отдельная лёгкая ВМ, этот дефолт резервируется на контейнер, а не делится между сервисами стека. Логирование: приложение (и davit compose) пишет в stderr на уровне, задаваемом переменной окружения DAVIT_LOG_LEVEL (trace|debug|info|notice|warning|error|critical, по умолчанию info); compose --verbose дополнительно переключает процесс в debug, если DAVIT_LOG_LEVEL не был явно (и корректно) задан.

  • Реестры (Registries) — войдите в Docker Hub / ghcr.io / любой реестр для загрузки приватных образов (Settings → Registries); учётные данные проверяются против реестра и сохраняются в login-keychain, совместно используемом с CLI container. Также поддерживаются Docker credential helpers: если в ~/.docker/config.json есть запись credHelpers/credsStore для реестра (Google Artifact Registry через gcloud, ECR и т.д.), Davit вызывает helper прямо перед каждой загрузкой и передаёт короткоживущий токен платформе, так что учётные данные с почасовым сроком действия всегда актуальны.

  • Элемент в строке меню (Menu bar extra) — статус сервисов, быстрые действия с контейнерами из любого места.

  • Автозапуск контейнеров (Auto-start containers) — отметьте контейнер «Запускать при открытии Davit» (контекстное меню); при старте приложение при необходимости поднимает сервисы платформы и запускает их. В сочетании с Открывать при входе, контейнеры будут работать, когда вы садитесь за Mac.

  • Открывать при входе (Open at login) — опциональный запуск в строку меню при входе в систему (Settings → General), реализован через SMAppService.

  • Обновления внутри приложения (In-app updates) — ежедневно проверяет GitHub Releases (или по запросу из About); одним кликом загружает новую версию, проверяет подпись Developer ID (команда должна совпадать), атомарно заменяет бандл с возможностью отката и перезапускает приложение. Davit update check|install в headless-режиме.

  • Настройки (Settings) — переопределение корня установки платформы и интервал обновления (General), а также полноценный редактор конфигурации платформы (platform configuration editor) (вкладка Platform): CPU/память контейнеров по умолчанию, реестр, локальный DNS-домен, ресурсы сборщика/Rosetta и расширенные параметры (ядро, init-образ, машина). В ~/.config/container/config.toml записываются только значения, отличающиеся от установочных по умолчанию; секции [plugin.*] сохраняются; каждое сохранение валидируется через собственный загрузчик конфигурации платформы перед фиксацией, и результат сразу публикуется в корень приложения — дефолты контейнеров/реестр/DNS применяются к новым операциям немедленно, настройки на стороне демона — после перезапуска сервиса (кнопка предусмотрена).

Требования

  • Mac с Apple silicon, macOS 15+ (рекомендуется macOS 26 — соответствует container 1.1)

  • Установленный apple/container (или vendored, см. ниже)

Релизы

При создании тега вида v* срабатывает GitHub Actions workflow, который собирает приложение на macOS-раннере и прикладывает Davit-<version>.dmg и Davit-<version>.zip (+ sha256) к GitHub Release. DMG предназначен для установки перетаскиванием в Applications; zip используется встроенным апдейтером и Homebrew.

При наличии следующих секретов репозитория релизы подписываются Developer ID (hardened runtime) и нотаризуются, так что открываются как любое другое приложение:

Секрет Значение

MACOS_CERT_P12

base64 экспортированного сертификата «Developer ID Application» (base64 -i cert.p12)

MACOS_CERT_PASSWORD

пароль экспорта .p12

APPLE_ID

email вашего Apple ID

APPLE_TEAM_ID

10-символьный team ID (developer.apple.com → Membership)

APPLE_APP_SPECIFIC_PASSWORD

app-specific пароль с appleid.apple.com

Без секретов сборки используют ad-hoc подпись, а в примечаниях к релизу добавляется инструкция с обходом через xattr -dr com.apple.quarantine.

Установка

brew install wouterdebie/tap/davit

(Homebrew 6 при первом использовании стороннего tap просит подтвердить доверие: brew trust wouterdebie/tap.)

Либо загрузите подписанный DMG из Releases / davit.app и перетащите Davit в Applications.

Быстрый старт

От чистой установки до работающего приложения в браузере:

  1. Откройте Davit. При первом запуске, если контейнерная платформа Apple не установлена, Davit загрузит и установит её за вас — пароль администратора не нужен.

  2. Загрузите демо-образ — Images → Pull Image → nginxdemos/hello (небольшой, отдаёт видимую страницу).

  3. Запустите с пробросом порта — запустите образ, пробросьте хостовый порт 8088 → контейнерный 80.

  4. Откройте в браузере — в строке Ports контейнера есть кнопка Open in Browser, или перейдите по localhost:8088. Вы увидите страницу, отданную изнутри контейнера, с его hostname и IP.

  5. Исследуйте — живые графики CPU/памяти/диска, стриминг логов, терминал в один клик и Edit & Recreate для изменения портов/переменных окружения/ресурсов.

Сборка и запуск

scripts/bundle.sh        # собирает release-бинарник + формирует dist/Davit.app
open dist/Davit.app

Для разработки также работает swift build / swift run (без бандла — нет иконки и строки меню).

Что обязательно должно быть установлено

API устраняет зависимость от CLI, но не от платформы: container-apiserver и бинарники плагинов runtime/сети — это launchd-сервисы хоста, с которыми библиотека лишь общается. Davit определяет корень установки платформы в следующем порядке:

  1. пользовательский корень из Settings

  2. управляемая Davit установка по пути ~/Library/Application Support/dev.wouter.davit/platform/<version>

  3. /usr/local (официальный установщик)

  4. vendored-версия внутри приложения по пути Davit.app/Contents/Resources/vendor

Установка внутри приложения (In-app install): если платформа не обнаружена, экран приветствия предлагает установку одним кликом — Davit загружает подписанный Apple установочный pkg (с живым прогресс-баром), проверяет подпись кода, извлекает payload в управляемый корень (права администратора не нужны, в отличие от официального установщика) и загружает оттуда сервисы. В Settings → General можно также установить shell-команду container: обёртку в /usr/local/bin, которая прописывает CONTAINER_INSTALL_ROOT в управляемый корень перед exec реального CLI (один запрос прав администратора; обычный symlink не подошёл бы, так как CLI вычисляет корень установки из нераскрытого пути до исполняемого файла). Управляемая копия намеренно стоит выше /usr/local в порядке поиска: она всегда соответствует версии клиента, с которой собрано приложение, и более новый системный демон не нарушит XPC-совместимость. В headless-режиме: Davit platform install|remove.

Чтобы поставить полностью автономное приложение, работающее без системного установщика:

scripts/vendor.sh 1.0.0        # скачивает официальный подписанный .pkg, извлекает payload в Vendor/container
scripts/bundle.sh --vendor     # упаковывает его в приложение

Запуск и остановка сервисов реализованы внутри процесса (LaunchPlist + ServiceManager из ContainerPlugin — тот же код, что выполняет container system start), направленные на разрешённый корень установки. Установка ядра и init-образа при первом старте происходит без интерактивного взаимодействия. Данные приложения хранятся в стандартном ~/Library/Application Support/com.apple.container/, так что Davit и CLI (если установлен) всегда видят одни и те же контейнеры.

Закрепление версии (Version pinning): зависимость SPM на apple/container закреплена через exact: "1.0.0" в соответствии с демоном; клиент и apiserver поставляются в связке, и XPC-протокол не является стабильным публичным API. При обновлении установленной платформы поднимите закреплённую версию и пересоберите.

Для полноты картины — компромиссы при использовании vendoring: бандл вырастает примерно на 150 МБ, вы сами управляете циклом обновления тулчейна, а launchd-сервисы регистрируются внутри бандла приложения (поэтому перемещение или удаление приложения осиротит их до выполнения container system stop). По умолчанию используется системная установка, потому что официальный pkg держит сервисы в /usr/local и обновляется независимо.

Бинарные режимы (Binary modes)

Бинарник приложения одновременно работает как небольшой инструмент командной строки:

Davit exec <container-id> [command…]   # интерактивная TTY-оболочка — или разовая команда — в контейнере (используется «Open Terminal»)
Davit selftest                   # сквозное тестирование слоя XPC-сервисов против живого демона
Davit system start|stop          # поднять / остановить launchd-сервисы контейнеров
Davit platform install|remove    # загрузить и проверить подписанный Apple pkg в управляемый корень установки
Davit registry login|list|logout # учётные данные реестра (login читает пароль из stdin)
Davit run [flags] IMAGE [COMMAND…]    # запуск одного контейнера в стиле docker
    # флаги предшествуют IMAGE (соглашение docker; -- также завершает разбор флагов, --help выводит справку и
    # завершается с кодом 0): -d|--detach, --rm, --pull missing|always|never, -e/--env, --env-file, -t/--tty,
    # -u/--user, --uid, --gid, -w/--workdir, --ulimit, -c/--cpus, -m/--memory, --name, -p/--publish,
    # -v/--volume, --mount, --tmpfs, --network, --entrypoint, -l/--label, --platform, --arch, --os,
    # --cap-add, --cap-drop, --init, --read-only, --shm-size, --dns, --dns-search, --dns-option,
    # --no-dns, --rosetta, --virtualization, --ssh (пробрасывает SSH_AUTH_SOCK из окружения хоста),
    # --cidfile (записывается, как только контейнер поднят; отказывает при наличии существующего файла,
    # паритет с docker), --verbose|-q|--quiet. Объединённые короткие флаги (`-it`, `-dit`, …) раскрываются
    # так же, как их принимает сам docker. Без -d подключается к логам нового контейнера (без префикса,
    # статусные строки — в stderr, чтобы stdout оставался чистым выводом контейнера) до Ctrl-C, который
    # только отключается — контейнер продолжает работать (сигналы не могут быть переданы гостевому
    # процессу на этой платформе, в отличие от docker) — и завершается с кодом выхода самого контейнера
    # после его остановки (паритет с docker); с --rm удаление происходит сразу после завершения attach,
    # а не гонки с автоудалением на стороне демона для быстрого одноразового контейнера, так что вывод
    # никогда не теряется. -i/--interactive и docker-флаги без маппинга на платформу (--restart,
    # --privileged, --add-host, --hostname, --gpus, …) отклоняются явно, а не игнорируются молча.
Davit compose <subcommand> [-f <file>] [--env-file <path>] [--profile <p>]… [service…]
    # подкоманды: plan | up [-d|--detach] | down [-v|--volumes] | ps | logs [-f|--follow] [--tail <n>]
    #              stop | start | restart | pull | exec <service> <command…>
    # файл обнаруживается автоматически, как в docker; интерполяция .env + ${VAR}; указание имён
    # сервисов сужает область (plan/up добавляют замыкание зависимостей; stop/start/restart/pull/ps/down
    # применяются ровно к указанным сервисам). Имена сервисов внутри контейнеров разрешаются через
    # управляемые записи /etc/hosts, синхронизируемые при каждом up/start/restart — пересоздания вне
    # compose требуют повторного up/start.
Davit build -t <tag> <dir>       # собрать образ из <dir>/Dockerfile через BuildKit-шим
Davit machine list|create|boot|stop|delete   # машины контейнеров (микро-ВМ)
Davit --snapshot /tmp/shots      # отрисовать все экраны в PNG через ImageRenderer (разрешение на запись экрана не нужно)

Архитектура

Sources/ContainerStack/
  Main.swift            точка входа + бинарные режимы exec/selftest/system
  Backend.swift         слой XPC-сервисов на ContainerAPIClient: фасад ContainerService,
                        SystemController (launchd bootstrap), LogStreamer (tail/follow по FileHandle),
                        PullProgressModel, TerminalLauncher, разрешение корня установки платформы
  Models.swift          модели представлений, сопоставленные с типами ContainerResource (вьюхи
                        остаются отвязанными от нестабильного API библиотеки)
  AppState.swift        хранилище @MainActor: опрос (4с — данные / 2с — статистика), вычисление CPU%,
                        действия
  SnapshotDriver.swift  механизм --snapshot
  App.swift             SwiftUI-сцены, элемент строки меню
  Views/                Shell (sidebar), Dashboard, Containers, ContainerDetail, Images,
                        VolumesNetworks, Sheets (run/pull/create), Settings, Components

Замечание о статистике: демон сообщает накопленный cpuUsageUsec; Davit вычисляет CPU% из дельт между опросами, нормализованных по реальному времени (100% = одно полное ядро).

© 2026 meganuke