A native macOS UI for Apple's container platform — think OrbStack/Docker Desktop, but for the Linux-containers-as-lightweight-VMs stack Apple ships for Apple silicon.
A davit is the shipboard crane that hoists cargo and small craft over the side — which is more or less what this app does with your containers.
Built entirely in SwiftUI (no Electron, no web views). Davit links Apple's own
ContainerAPIClient library and talks to container-apiserver directly over XPC —
the same wire path the container CLI uses. The CLI binary is never invoked: lists,
lifecycle, live stats, log streaming, image pulls, volume/network management, the
in-terminal shell (davit exec), and even launchd service bootstrap all go through the API.
- Dashboard — service status with one-click start/stop, resource counts, disk usage with reclaimable-space cleanup menu, live aggregate CPU chart across running containers.
- Containers — list with live CPU/memory/IP per row, start/stop/kill/restart/delete, prune, search, and Edit & Recreate (containers are immutable on this platform, so "editing" opens the Run sheet prefilled with the container's ports/env/mounts/resources/network — with the image's entrypoint/CMD/env subtracted so only your customizations show — and replaces it on confirm). Detail view with:
- Overview: image, command, platform, resources, network (IP/MAC/gateway/hostname), published ports with "Open in Browser", mounts, environment, labels.
- Logs: streaming
-ffollow mode, boot-log toggle, tail selector, copy. - Stats: live CPU %, memory, and disk-I/O charts (Swift Charts); tiles for CPU, memory, disk space used, network, and process count.
- Inspect: pretty-printed raw JSON.
- Terminal: opens an interactive shell in Terminal/iTerm (
davit execover XPC). - Files: browse the container filesystem — navigate, download, upload, delete (over
exec+ copy in/out).
- Images — pull with streaming progress, Pull Latest to refresh any image's tag to the newest digest (context menu or image detail), run-from-image, tag, delete, prune; per-image platform variants, size, digest, "used by" containers.
- Volumes — create (with size), delete, prune, reveal backing image in Finder, in-use badges.
- Networks — create (subnet / internal), delete, prune, attached-container counts.
- Machines — Apple's container machines (lightweight general-purpose VMs with your home directory mounted and a stable
.machineDNS name): create from any image with CPU/memory sizing, boot/stop, set the default, delete; detail view with configuration overview, streamed logs, live stats and inspect JSON; one-click Terminal into the machine's login shell; edit CPUs/memory/home-mount (applies on next boot).Davit machine list|create|boot|stop|delete|exec|setheadless. - Run Container sheet — image picker, name, command, ports, env vars, volume/bind mounts, CPU/memory limits, network selection.
- Build images — Images → Build Image: pick a context folder and Dockerfile, set tag/build-args, and Davit drives the platform's BuildKit builder (starting it if needed), then loads and tags the result into the image store.
Davit build -t <tag> <context-dir>headless. - Compose import — Containers → ⋯ → Import Compose File: parses a compose file, previews exactly what will be created (services in
depends_onorder, named volumes, networks, the equivalentcontainer runcommand per service, and warnings for anything unsupported), then creates and starts the stack — honoringhealthchecks,profiles,depends_onconditions (service_healthy,service_completed_successfully),env_file:/entrypoint:, and${VAR}interpolation from the file's sibling.envmerged under the process environment. Headless: the full lifecycle —Davit compose plan | up [-d] | down [-v] | ps | logs [-f\|--follow] [--tail <n>] | stop | start | restart | pull | exec <service> <command…>, each taking[-f <file>] [--env-file <path>] [--profile <p>]… [service…]— autodiscovers the file like docker (compose.yaml,compose.yml,docker-compose.yml,docker-compose.yaml, walking up parent directories;COMPOSE_FILEwins). Naming services scopes the command:plan/upadd the dependency closure, whilestop/start/restart/pull/psanddownapply to exactly the named services (docker behavior). Service discovery: the platform's DNS cannot resolve compose service names, soup/start/restartwrite managed/etc/hostsentries (suffixed# davit-compose) into every running container of the project, mapping each service and container name to its current IP — refreshed on every run, so a recreated service's new IP reaches the containers that kept running. Caveat: containers recreated outside compose keep stale entries until the nextuporstart; images without/bin/shcan't be patched (warning). - Registries — sign in to Docker Hub / ghcr.io / any registry to pull private images (Settings → Registries); credentials are verified against the registry and stored in the login keychain, shared with the
containerCLI. Docker credential helpers are honored too: if~/.docker/config.jsonhas acredHelpers/credsStoreentry for a registry (Google Artifact Registry viagcloud, ECR, etc.), Davit invokes the helper right before each pull and stages the short-lived token for the platform, so hourly-expiring credentials always work. - Menu bar extra — service status, per-container quick actions from anywhere.
- Auto-start containers — mark any container "Start when Davit Opens" (context menu); on launch Davit brings the platform services up if needed and starts them. Combined with Open at login, your containers are running when you sit down.
- Open at login — optional launch-to-menu-bar at login (Settings → General), via
SMAppService. - In-app updates — checks GitHub Releases daily (or on demand from About); one click downloads the new version, verifies its Developer ID signature (team must match), swaps the bundle atomically with rollback, and relaunches.
Davit update check|installheadless. - Settings — platform install-root override and refresh interval (General), plus a full platform configuration editor (Platform tab): default container CPUs/memory, registry, local DNS domain, builder resources/Rosetta, and advanced knobs (kernel, init image, machine). Only values differing from install defaults are written to
~/.config/container/config.toml,[plugin.*]sections are preserved, every save is validated through the platform's own config loader before commit, and the result is published to the app root immediately — container defaults/registry/DNS apply to new operations right away, daemon-side settings after a service restart (button provided).
- Apple silicon Mac, macOS 15+ (macOS 26 recommended — matches
container1.1) - apple/container installed (or vendored, see below)
Tagging v* triggers a GitHub Actions workflow that builds the app on a macOS
runner and attaches Davit-<version>.zip (+ sha256) to a GitHub Release.
With these repository secrets set, releases are Developer ID signed (hardened runtime) and notarized, so they open like any other app:
| Secret | Value |
|---|---|
MACOS_CERT_P12 |
base64 of the exported "Developer ID Application" cert (base64 -i cert.p12) |
MACOS_CERT_PASSWORD |
the .p12 export password |
APPLE_ID |
your Apple ID email |
APPLE_TEAM_ID |
10-char team id (developer.apple.com → Membership) |
APPLE_APP_SPECIFIC_PASSWORD |
app-specific password from appleid.apple.com |
Without the secrets, builds fall back to ad-hoc signing and the release notes
include the xattr -dr com.apple.quarantine workaround.
brew install wouterdebie/tap/davit(Homebrew 6 asks you to trust third-party taps on first use: brew trust wouterdebie/tap.)
Or download the signed zip from Releases / davit.app.
From a fresh install to something running in your browser:
- Open Davit. On first launch, if Apple's container platform isn't installed, Davit downloads and installs it for you — no admin password needed.
- Pull a demo image — Images → Pull Image →
nginxdemos/hello(small, and it serves a visible page). - Run it with a port — Run the image, map host
8088→ container80. - Open it — the container's Ports row has Open in Browser, or visit
localhost:8088. You'll see a page served from inside the container, showing its own hostname and IP. - Explore — live CPU/memory/disk, streaming logs, a one-click terminal, and Edit & Recreate to change ports/env/resources.
scripts/bundle.sh # builds release binary + assembles dist/Davit.app
open dist/Davit.appPlain swift build / swift run also works for development (no bundle, so no icon/menu-bar niceties).
The API removes the CLI dependency, not the platform dependency: container-apiserver
and its runtime/network plugin binaries are host launchd services that the library only
talks to. Davit resolves the platform install root in this order:
- custom install root from Settings
- a Davit-managed install at
~/Library/Application Support/dev.wouter.davit/platform/<version> /usr/local(the official installer)- vendored inside the app at
Davit.app/Contents/Resources/vendor
In-app install: when no platform is found, the onboarding screen offers one-click
install — Davit downloads Apple's signed installer pkg (with a live progress bar),
verifies the code signature, extracts the payload into the managed root (no
administrator rights needed, unlike the official installer), and bootstraps the
services from there. Settings → General can then also install a container shell
command: a wrapper in /usr/local/bin that pins CONTAINER_INSTALL_ROOT to the
managed root before exec'ing the real CLI (one admin prompt; a bare symlink would be
wrong, since the CLI derives its install root from the unresolved executable path). The managed copy sits
above /usr/local in the resolution order on purpose: it always matches the client
version the app links, so a newer system daemon can't break XPC compatibility.
Also available headless: Davit platform install|remove.
To ship a fully self-contained app that works without the system installer:
scripts/vendor.sh 1.0.0 # downloads the official signed .pkg, extracts payload into Vendor/container
scripts/bundle.sh --vendor # bundles it into the appService start/stop is implemented in-process (LaunchPlist + ServiceManager from
ContainerPlugin — the same code container system start runs), pointed at whichever
install root was resolved. Kernel and init-image installation are handled
non-interactively on first start. App data lives in the standard
~/Library/Application Support/com.apple.container/, so Davit and the CLI (if
installed) always see the same containers.
Version pinning: the SPM dependency on apple/container is pinned exact: "1.0.0" to
match the daemon; client and apiserver ship in lockstep and the XPC protocol is not a
stable public API. When the installed platform updates, bump the pin and rebuild.
Trade-offs of vendoring, for the record: the bundle grows by ~150 MB, you own the
update cadence of the toolchain, and the launchd services are registered from inside
the app bundle (so moving/deleting the app orphans them until container system stop).
The default remains "use the system install" because the official pkg keeps services
under /usr/local and updates independently.
The app binary doubles as a small tool:
Davit exec <container-id> [command…] # interactive TTY shell — or a one-off command — in a container (used by "Open Terminal")
Davit selftest # end-to-end test of the XPC service layer against the live daemon
Davit system start|stop # bootstrap / tear down the container launchd services
Davit platform install|remove # download + verify Apple's signed pkg into an app-managed install root
Davit registry login|list|logout # registry credentials (login reads the password from stdin)
Davit compose <subcommand> [-f <file>] [--env-file <path>] [--profile <p>]… [service…]
# subcommands: plan | up [-d|--detach] | down [-v|--volumes] | ps | logs [-f|--follow] [--tail <n>]
# stop | start | restart | pull | exec <service> <command…>
# file autodiscovered like docker; .env + ${VAR} interpolation; naming services scopes the
# command (plan/up add the dependency closure; stop/start/restart/pull/ps/down take exactly
# the named services). In-container service names resolve via managed /etc/hosts entries,
# re-synced on every up/start/restart — recreates outside compose need another up/start.
Davit build -t <tag> <dir> # build an image from <dir>/Dockerfile via the BuildKit shim
Davit machine list|create|boot|stop|delete # container machines (micro VMs)
Davit --snapshot /tmp/shots # render every screen to PNGs via ImageRenderer (no screen-recording permission)Sources/ContainerStack/
Main.swift entry point + exec/selftest/system binary modes
Backend.swift XPC service layer on ContainerAPIClient: ContainerService facade,
SystemController (launchd bootstrap), LogStreamer (FileHandle tail/follow),
PullProgressModel, TerminalLauncher, platform install resolution
Models.swift view models mapped from ContainerResource types (views stay decoupled
from the unstable library API)
AppState.swift @MainActor store: polling (4s data / 2s stats), CPU% derivation, actions
SnapshotDriver.swift --snapshot harness
App.swift SwiftUI scenes, menu bar extra
Views/ Shell (sidebar), Dashboard, Containers, ContainerDetail, Images,
VolumesNetworks, Sheets (run/pull/create), Settings, Components
Stats note: the daemon reports cumulative cpuUsageUsec; Davit derives CPU% from deltas
between polls, normalized to wall-clock time (100% = one full core).