A native, Docker Desktop-style macOS app for Apple's container CLI — manage containers, images, volumes, networks and machines from a fast SwiftUI interface instead of the terminal. It also runs Windows and Linux virtual machines (experimental), and AI agents can drive it over MCP.
Container Desktop does not reimplement any container logic: every action shells out to the official container CLI and parses its JSON output, so what you see is always exactly what the CLI would tell you.
📘 Documentation — Compose guide, x-init tasks, host.containers.internal, troubleshooting.
- Card or table view for the list: cards give each container a state tile, live CPU and memory meters, its ports as clickable chips and the controls that fit its current state; the table stays one click away for dense scanning
- List with live state, IP address and published ports (with one-click open in browser arrows); details panel with logs, live statistics, file browser, inspect and an embedded terminal (
exec -itvia SwiftTerm) - Native log viewer: continuous selection, copy-all, optional timestamps and severity colouring (errors red, warnings orange) tolerant of serilog/.NET/logfmt/klog syntaxes
- Start / stop / restart / kill / remove / prune, with per-container progress indicators
- Rich Run container dialog: ports, environment variables, volumes (pick an existing volume or a local folder), resources, network, architecture (arm64 / amd64 with automatic Rosetta), entrypoint,
--rm— with a live, copy-pasteable shell preview of the exact command - Change command / configuration of an existing container: the app recreates it with the same configuration pre-filled for editing (volume data survives)
- Live image-pull progress streamed straight into the dialog
- Docker Compose: paste a
docker-compose.ymland the app translates it intocontainer runcalls — project network, dependency ordering, grouped containers on the list with start/stop-all. One-off setup tasks (e.g. database creation) via thex-init: trueservice extension run to completion before the other services start. Thehost.containers.internalalias resolves to the project network's gateway (your Mac as seen from containers), and service hostnames are wired between containers via/etc/hosts(works around the broken name DNS in container 1.0.0). A "skip init tasks" toggle reruns a stack without repeating one-off setup
Logs with severity colouring, and a full terminal inside the container:
| Log viewer | Embedded terminal |
|---|---|
![]() |
![]() |
- CPU %, memory, network and disk throughput (per second), process count — refreshed every second
- Native Swift Charts with a selectable time window (1–15 min) and hover tooltips snapped to samples
- Pull and build (Dockerfile) with streamed progress, run-from-image, tag / delete / prune / inspect
- Volumes and networks: create, delete, prune, inspect; volume file browser
- Registry logins (password passed securely via stdin)
- Container machines: create, set default, stop, delete
- Local clusters through the
container k8splugin (container 1.2.1+, marked EXPERIMENTAL by Apple): create with a chosen name, CPU, memory and node image, start, delete, and load a locally built image straight into the cluster's containerd (load-image) so pods can useimagePullPolicy: Never - Cluster creation streams its progress — the first run downloads an ~850 MB node image and waits for kubeadm
- If the CLI was upgraded but the background service still runs the older build, the section says so and offers a one-click service restart instead of a cryptic XPC error
- Helm: repositories (add / remove / update), chart search across them, and releases with upgrade, rollback through
helm history, and uninstall - Dynamic values editor built from the chart's own
values.yaml: a generated form with type-aware controls, help text harvested from the file's comments, a key filter, and a raw-YAML tab that stays in sync. Only the keys you actually change end up in-f, so chart defaults keep moving with chart upgrades. Check (dry run) renders the release on the cluster first, surfacing template errors andvalues.schema.jsonviolations before anything is applied - Every helm command is pinned to the selected cluster with an app-managed kubeconfig — your
~/.kube/configis never read or modified, so a GUI action cannot reach a production cluster
Beta, work in progress. Needs QEMU (
brew install qemu). Things may change between versions.
- Full Windows 11 ARM64 and Ubuntu 24.04 machines in QEMU with Apple's Hypervisor framework, in their own Virtual Machines section. They run detached, so they keep going when the app quits.
- Windows installs itself from your own ISO (download it from Microsoft): no questions and no clicks, about 12 minutes to a logged-in desktop. Ubuntu starts from the official cloud image with cloud-init: about 30 s for a server, about 3 minutes with the GNOME desktop.
- Live preview in its own window, through a built-in VNC client: mouse, keyboard (Cmd is the Windows key, right Option is AltGr), Ctrl+Alt+Del and other shortcuts a Mac keyboard can't send, a screenshot to the clipboard, and a resolution that follows the window.
- Cards with screen thumbnails, per-machine settings (CPU, memory, resolution), snapshots, linked clones and delete.
- Windows on ARM64 gets our own build of the QEMU guest agent, with VSS. virtio-win ships it for x86/x64 only, so the source and build script are in
scripts/qemu-ga-windows-arm64.
- The app ships an MCP server (
cd-mcp, inside the app bundle). Claude Code, Codex, Cursor and other agents can list, run, stop and inspect containers, and manage images, volumes, networks and machines. Settings → AI Agents → Add to Claude Code registers it, or copy the JSON config for any other client. - An agent can also drive a virtual machine: screenshot, click, type, keys, run commands, copy files and snapshots. This only works on machines where you turned on Allow an AI agent to control this machine, which is off by default.
- An agent gets roughly your own reach over the container service. Allow it only for agents you trust.
- A WidgetKit widget in three sizes: running count with service health (small), a live list with CPU per container (medium), and everything grouped by Compose project (large).
- A widget extension is always sandboxed and cannot run the
containerCLI, so the app writes a snapshot after each refresh and the widget reads it — which also means the widget keeps showing the last known state while the app is closed, and says how old it is.
- Service status with safe start/stop (verifies the result — the CLI swallows some failures), disk usage with reclaimable-space bars, builder management, local DNS domains (admin prompt handled), readable system properties and a built-in service log viewer
- Liquid Glass accents, colorful System Settings-style sidebar, transition states everywhere (“Stopping… (stopping containers)”), informative empty states and (i) explainers across the app
- Menu bar extra: service status, running containers with one-click stop, “stop all”, jump to any section
- Localized in English, Polish and Simplified Chinese (简体中文) — follows the system language, with an in-app language switch in Settings (System / English / 中文 / Polski). Systems set to any other language default to English.
- Automatic updates via Sparkle: Check for Updates… in both the app and menu-bar menus, with an EdDSA-signed appcast served from GitHub Pages
- macOS 26 (Tahoe) on Apple Silicon
containerCLI installed (default:/usr/local/bin/container; a custom path can be set in app Settings)- Optional:
helmfor the Helm section (brew install helm), andcontainer1.2.1+ for local Kubernetes clusters - Optional: QEMU for virtual machines (
brew install qemu)
Download the DMG from Releases, open it and drag Container Desktop to Applications.
Note on Gatekeeper: releases are currently not notarized (no paid Apple Developer membership). On first launch macOS will warn that the app is from an unidentified developer. Open System Settings → Privacy & Security and click Open Anyway, or remove the quarantine flag manually:
xattr -dr com.apple.quarantine "/Applications/ContainerGUI.app"Alternatively, build from source — it takes one command.
Once installed, Container Desktop checks for updates with Sparkle (use Check for Updates… in the app or menu-bar menu). Updates are verified with an EdDSA signature and Sparkle clears quarantine on the installed update, so after the first launch you won't need the Gatekeeper step again. Maintainer release flow (sign/notarize if available → generate_appcast → publish DMG + docs/appcast.xml) is documented in scripts/package.sh.
The update check is the only thing Container Desktop sends anywhere, and since 0.8.3 it carries three values: a random identifier generated once on this installation, the app version, and the macOS version. They exist so the project can tell how many people use it and on which version — a question the appcast's own traffic cannot answer, because one machine checking daily looks like thirty machines checking once.
It does not send your IP address, your computer's name, your account, or anything about the hardware. Sparkle's own system profile — which reports the Mac model, the CPU and the memory — stays switched off. Turn the whole thing off in Settings → Privacy → Anonymous statistics; updates keep working.
Od 0.8.3 program przy sprawdzaniu aktualizacji wysyła losowy identyfikator instalacji, swoją wersję i wersję macOS — po to, by dało się policzyć, ilu ludzi go używa. Bez adresu IP, bez nazwy komputera, bez danych o sprzęcie. Wyłącznik: Ustawienia → Prywatność → Anonimowe statystyki.
brew install xcodegen
git clone https://github.com/sembsa/ContainerDesktop.git && cd ContainerDesktop
xcodegen generate
xcodebuild -project ContainerGUI.xcodeproj -scheme ContainerGUI \
-destination 'platform=macOS' -derivedDataPath .build build
open .build/Build/Products/Debug/ContainerGUI.appTests:
xcodebuild test -project ContainerGUI.xcodeproj -scheme ContainerGUI \
-destination 'platform=macOS' -derivedDataPath .buildDistributable DMG (ad-hoc signed, no notarization):
scripts/package.sh # produces dist/ContainerDesktop.dmgSwiftUI views (per feature) → @Observable stores → ContainerCLI (actor) → Process(container CLI)
│ │ │
MenuBarExtra AppModel (root) JSON (Codable) / line streams (AsyncStream)
Embedded terminal (SwiftTerm, PTY)
ContainerGUI/CLI— process execution with timeouts and watchdog, argv builder with a shell-style tokenizer, streaming line readerContainerGUI/Models—Codablemodels mapped to real CLI JSON outputContainerGUI/Features/*— one store + views per sectionContainerGUI/Features/VirtualMachinesandContainerGUI/Common/RFB: the QEMU engine (QMP and guest-agent clients, launcher, installers) and a native RFB/VNC clientContainerDesktopMCP: the stdio MCP server, compiled from the app's own CLI and engine sources- App runs outside the App Sandbox (it spawns an external CLI), so distribution is direct (DMG), not the Mac App Store
Container Desktop is free. If it saves you time, you can sponsor it on GitHub or buy me a coffee on Ko-fi ☕







