A native SwiftUI desktop app for Apple's container tool.
Run and inspect Linux containers, manage images, volumes & networks, and control the
system service — with live resource charts and a ⌘K command palette, from a polished Mac UI.
ContainerUI is a thin, robust GUI on top of the container command-line tool. It shells out to the supported CLI and decodes its --format json output, so it stays compatible across container releases instead of binding to internal APIs.
- macOS 26 on Apple silicon — required by
containeritself. - Apple's
containertool installed (the signed installer places it at/usr/local/bin/container). - Xcode 16+ and XcodeGen to build from source.
The app deploys to macOS 14 so it can launch and show a friendly "not installed" screen, but the backend it drives needs macOS 26.
Install the latest release with Homebrew:
brew install --cask kylemclaren/tap/container-uiThe owner/tap/name form auto-taps kylemclaren/homebrew-tap, so it's a single step — no separate brew tap needed. From then on the app updates itself in place.
Prefer a direct download? Grab the DMG from the latest release, drag ContainerUI to Applications, and open it.
ContainerUI isn't code-signed or notarized yet (that needs a paid Apple Developer account — see Signing & distribution). Because macOS quarantines anything downloaded, Gatekeeper blocks the first launch and reports the app as "damaged" or from an "unidentified developer." This applies to both install paths — Homebrew 5.1 removed the --no-quarantine flag, so brew install --cask no longer clears it for you.
The reliable fix is to strip the quarantine attribute once, after the app is in /Applications:
xattr -dr com.apple.quarantine /Applications/ContainerUI.appThen open it normally. (Right-click → Open works for the "unidentified developer" prompt, but not for the "damaged" message that Apple silicon shows for unsigned apps — use the command above.)
Nothing is wrong with the download; this is just how macOS treats software from developers who haven't paid into Apple's notarization program. Once ContainerUI ships notarized builds, this step goes away.
This repo doesn't check in an .xcodeproj; it's generated from project.yml with XcodeGen.
brew install xcodegen # one-time
xcodegen generate # creates ContainerUI.xcodeproj
open ContainerUI.xcodeproj # then Run (⌘R) in XcodeOr from the command line:
xcodegen generate
xcodebuild -scheme ContainerUI -configuration Debug buildxcodegen generate
xcodebuild -scheme ContainerUI -destination 'platform=macOS' testThe unit tests are host-less logic tests: the test bundle compiles the UI-independent Core/ sources directly and exercises them with a mock command runner and recorded CLI JSON fixtures — no live container backend, no GUI launch. They cover JSON decoding, exact argv construction, error classification, and formatting.
The app talks to container through one small, testable seam.
SwiftUI Views ──▶ @Observable ViewModels ──▶ Services ──▶ ContainerCLI ──▶ CommandRunner
(Features/) (per feature) (Core/Services) (Core/CLI) (Process)
│
MockCommandRunner ◀──────────┘ (tests)
CommandRunner(Core/CLI) — a protocol over process execution.ProcessCommandRunnerruns the real binary;MockCommandRunnerfeeds fixtures in tests.ContainerCLI— resolves thecontainerbinary, runs invocations, classifies failures into a typedCLIError, and decodes JSON with a decoder configured to match the CLI's encoder (ISO-8601 dates, no key-strategy remapping).- Services —
ContainerService,ImageService,VolumeService,NetworkService,SystemServicebuild exact argv (via pure, unit-tested…Arguments(…)functions) and returnCodablemodels. - Models (
Core/Models) — Swift structs that mirror the CLI's JSON shapes exactly, including the OCI config's PascalCase keys andsnake_casediff_ids/created_by. - Design system (
DesignSystem/) — a small component library (soft pill controls, translucent material cards with gradient hairlines, status badges, springy motion) shared across screens.
container's Swift packages are only API-stable within patch versions, and talking to its XPC apiserver directly requires matching entitlements and tracks internal changes. The CLI is the documented, stable surface, and almost every read command supports --format json. Shelling out keeps this app decoupled and resilient — the same approach mature Docker/Podman GUIs take.
| Area | Capabilities |
|---|---|
| Containers | List (all/running), live CPU/memory charts with real CPU % derived from counter deltas, inspect, start/stop/restart/kill, delete, prune, streaming logs, exec, a full Run form (shell-style quoting supported), and a one-click console that opens an interactive shell in your terminal app (Terminal, iTerm2, Ghostty, Warp, kitty, Alacritty, WezTerm — pick in Settings). |
| Images | List, inspect (config/env/layers/platforms), Pull with streaming progress, run a container straight from an image, tag, delete, prune. |
| Volumes | List, inspect, create (with size/format), delete, prune. |
| Networks | List, inspect (subnets/gateway), create (NAT or host-only, custom subnets), delete, prune — with the built-in network protected. |
| System | Service status, versions, disk usage, reclaim-space pruning, and start/stop the system service. |
| Everywhere | ⌘K command palette (fuzzy search across every container, image, volume, network, and action), menu-bar quick controls, right-click context menus, and ⌘R refresh of the active screen. |
The app auto-detects the container binary (and lets you set a custom path in Settings), and surfaces a clear state when the tool isn't installed or the service isn't running.
App/ App entry, root navigation, sidebar, settings, backend state
Core/
CLI/ CommandRunner, ContainerCLI, CLIError, executable resolution
Models/ Codable models + formatting + image-reference parsing
Services/ ContainerService, ImageService, SystemService
DesignSystem/ Theme tokens + reusable components
Features/
Containers/ Containers screen, row, detail, logs, run
Images/ Images screen, row, detail, pull, tag
Volumes/ Volumes screen, row, detail, create
Networks/ Networks screen, row, detail, create
System/ System screen
Resources/ Assets, entitlements, generated Info.plist
Tests/ Logic tests + fixtures + mock runner
project.yml XcodeGen project definition
The app runs unsandboxed (see Resources/ContainerUI.entitlements) because it spawns /usr/local/bin/container, which talks to a launchd service over XPC — the App Sandbox blocks that. Distribute with Developer ID + notarization (Hardened Runtime is enabled), not the Mac App Store. Set your DEVELOPMENT_TEAM in project.yml, or sign "to run locally" in Xcode for development.
Two GitHub Actions workflows live in .github/workflows/:
ci.yml— on every push tomainand every PR: generates the project, builds, and runs the full test suite.release.yml— on a pushedv*tag (or a manual run): builds a Release archive, optionally signs + notarizes it, wraps it in a DMG, and publishes a GitHub Release with the DMG and aSHA256SUMS.txtattached.
git tag v1.2.0 # the leading "v" is stripped; 1.2.0 becomes the app's version
git push origin v1.2.0The workflow stamps MARKETING_VERSION from the tag, so the built app's version always matches the release. A tag containing a hyphen (e.g. v1.2.0-beta1) is published as a pre-release. You can also trigger release.yml manually from the Actions tab (with an optional version input) to produce a DMG artifact without publishing a release.
The release pipeline degrades gracefully — with no secrets set it still builds and publishes an unsigned DMG. To ship a Gatekeeper-clean, notarized build, add these repository secrets (Settings → Secrets and variables → Actions):
| Secret | What it is |
|---|---|
MACOS_CERTIFICATE_BASE64 |
Your Developer ID Application certificate + private key exported as a .p12, then base64-encoded (base64 -i cert.p12 | pbcopy). |
MACOS_CERTIFICATE_PASSWORD |
The password you set when exporting the .p12. |
APPLE_TEAM_ID |
Your 10-character Apple Developer Team ID. |
APPLE_ID |
The Apple ID email used for notarization. |
APPLE_APP_SPECIFIC_PASSWORD |
An app-specific password for that Apple ID (used by notarytool). |
Behavior by what's present:
-
All five → signed, notarized, stapled. Opens with a double-click.
-
Just the certificate three (
MACOS_CERTIFICATE_*,APPLE_TEAM_ID) → signed but not notarized. First launch needs a right-click → Open. -
None → unsigned. Clear the quarantine flag after copying to
/Applications:xattr -dr com.apple.quarantine /Applications/ContainerUI.app
The release notes generated for each build spell out which of these applies.
The workflow just calls scripts you can run yourself:
VERSION=1.2.0 scripts/build-release.sh # unsigned; produces dist/ContainerUI-1.2.0.dmg
# or, signed, if a Developer ID identity is in your keychain:
VERSION=1.2.0 SIGNING_IDENTITY="Developer ID Application: You (TEAMID)" TEAM_ID=TEAMID scripts/build-release.shv1. This is an independent project and is not affiliated with or endorsed by Apple. It complements the container tool, which remains under active development.