Containers and images
Run, inspect and clean up containers and images, follow their logs and builds, and see live CPU, memory, network and disk usage on the dashboard.
A native Mac app for containers, machines, Kubernetes clusters and local AI models, built on Apple’s container.
container runs, in one windowRun, inspect and clean up containers and images, follow their logs and builds, and see live CPU, memory, network and disk usage on the dashboard.
Wire MLX, Ollama or LM Studio models into containers. Inference stays on your Mac’s GPU, and agents run behind a hypervisor boundary with a local endpoint instead of an API key.
Create, configure and monitor persistent Linux VMs from any init-capable image, with CPU, memory, home-mount and custom-kernel options.
Local clusters on container’s k8s plugin: pick the version, workers and CNI, load local images into every node, and get a working kubectl context in one click.
Bring a compose.yaml up in dependency order. Stacks you started in the terminal show up too, and anything the runtime can’t honour is listed before the first run.
Container traffic leaves your Mac like any app’s, so VPNs, DNS settings and per-app firewalls apply to it. Published ports and host.containers.internal just work.
Push, sign and verify images with Sigstore cosign, gate signing on Trivy vulnerability reports, and see each image’s signature at a glance.
Written in Swift and driven over Apple’s XPC APIs rather than by shelling out, so it starts instantly and stays in step with the runtime.
https://updates.verfüg.bar/orchard/appcast.xml for updates, signed with verfüg.bar's own EdDSA key, instead of upstream's feed: releases built on git.stei.gr are signed with Developer ID and notarized, and only they are offered. Each release is also listed on orchard.verfüg.bar.default network is a userspace network, like slirp or pasta on Linux: a container started without a network, from Orchard, the container CLI, docker or Compose, sends its traffic through a process on your Mac, so a VPN handles it like any app's. Userspace and bridge no longer keeps default on vmnet: containers created in Orchard join default and a separate vmnet network for inbound traffic (bridge, host-only by default). No usernet network is created any more; networks named usernet or usernet-… remain userspace networks.diagnose.sh (plugins/container-runtime-usernet) collects the host's VPN, routes, DNS and proxy settings and runs the same tests from the host, a usernet container and a default container.container-runtime-linux in container's user plugins, where it shadows Apple's, so every container starts in it and gets userspace networking on a usernet network, with no --runtime flag. With Userspace chosen, container's builtin default network is a userspace one too (a networks file in container's plugin state, written by network setup), so a plain docker run or container run gets it. Containers on other networks run exactly as with Apple's runtime.container-runtime-usernet, a runtime plugin for Apple's container that gives a container a userspace network (gvisor-tap-vsock, the stack behind Podman's gvproxy and Lima): its connections leave your Mac as an app's own sockets, so VPNs, your DNS settings and per-app firewalls apply to them. Published ports work; the network's gateway answers DNS with your Mac's resolver, and host.containers.internal reaches your Mac.container image push (over plain HTTP to a registry on localhost) and signs repository@digest, the exact digest the image list shows, never a tag that could have moved. Images matching a signing pattern are signed every time Orchard pushes them.cosign.key (the password is checked by deriving its public key). The password reaches cosign in its environment, never its arguments. By default key signatures stay out of Sigstore's public transparency log, so a private image's name doesn't become public; recording them there is a setting. Keyless signing logs in through the browser and is recorded publicly.verified, signed, or an unverified warning), and on its vulnerability report.~/.sigstore for its cache.* and ? wildcards, docker.io/library/ optional, a pattern without a tag matches every tag) are scanned every time Orchard builds them, and whenever they appear or change in the image list (a pull, a build from the Docker API or the CLI, a new digest under the same tag). Any image can be scanned by hand with Scan on its detail. Scans run one at a time.~/Library/Caches/trivy), shared with Trivy run from a terminal.orchard Docker context), container DNS (and its servers), and build resources (Apple's builder's CPUs and memory, and an optional orchard buildx builder with its own). A numbered summary lists every step Install takes, the privileged one marked: installing container and, with container DNS, the DNS helper, behind one administrator prompt. Once container runs, the choices are saved and applied: socktainer is downloaded and started, the buildx builder created on it, and container DNS turned on; anything that didn't work is said at the end. Nothing changes before Install, and everything can be changed later in Settings.kernel.url. Orchard now remembers the archive, its Kata release and the kernel in it, and Settings > System labels those properties as the recommended kernel's..tar.zst) on a Mac without Homebrew's zstd: it read the archive with macOS's tar, which has no zstd of its own, and reported "the download isn't an archive tar can read" (seen with kata-static-4.3.1-arm64.tar.zst). It now reads it with Containerization's archive reader, as container system kernel set does. A download's last progress update no longer turns a finished detection back to "Downloading", and an unreadable archive is no longer reported as an installer failure.container on the Mac, the main window offers to install it instead of showing that the system isn't running: Orchard downloads Apple's installer for the newest container release Orchard is built for (1.6.x) from GitHub, checks it against the checksum GitHub publishes and with pkgutil (signed with Apple's Containerization Developer ID Installer certificate, and notarized), installs it behind one administrator prompt, and starts it. The wizard also chooses the Linux kernel: container's recommended one, or one from a Kata Containers release whose URL you paste. Orchard downloads that archive, lists the kernels in it, preselects Kata's default (vmlinux.container) and installs the one you choose with container system kernel set, warning when the archive is for the other architecture. Check Again looks for a container installed by hand.orchard Docker context points at socktainer's socket and can be made Docker's current context, and turning that or the Docker API off switches back to the context you used before. If another socktainer, such as Whalebridge's, already serves the socket, Orchard leaves it alone. The integration follows Whalebridge./etc/resolver DNS work in containers as they do on your Mac, and nothing needs configuring; you can still enter DNS servers to use instead. This also fixes containers that can't resolve names because another DNS server on your Mac, such as unbound, dnsmasq or Cloudflare WARP, holds port 53. The helper only opens port 53 on gateway addresses and hands it to Orchard, which does the answering, and it serves only your user. Answers run while Orchard is open.docker build through the Docker API, Orchard's image builds and container build; Apply restarts it at that size. An orchard buildx builder can be created there too, for multi-platform builds and cache export (docker buildx build --builder orchard --platform linux/arm64,linux/amd64 …), with its own CPUs and memory; its cache is kept when you change its size.scripts/docker-api-test.sh tests the Docker API end to end: linux/arm64 and linux/amd64 images through docker build and through buildx, the buildx builder's CPU and memory limits, and build cache reuse, both within a builder and through an exported cache imported after a prune.<container>:latest. A running container is snapshotted first and keeps running. The image holds the container's filesystem as one layer and starts the way the container did, with the same command, user, environment, working directory, labels and stop signal; ports, volumes and the healthcheck aren't carried over. The container's executable becomes the image's entrypoint, so running a different program means overriding the entrypoint, not just the command. If an image with that name already exists, the sheet says so and asks before replacing it. The sheet shows each step while it runs and, when it's done, links to the new image in the Images tab and can run it. Orchard does this through the same client libraries as container commit, without running the CLI.ubuntu or UID 1000 on Ubuntu images, or the machine can't set up the account when it first boots.post_start and pre_stop hooks (#126), the same way the compose plugin does. Each post_start hook runs in its container after it starts, and each pre_stop hook runs before the container stops on down and when a service is recreated. While a hook runs, its service's row shows the hook and its latest line of output. A failing post_start hook stops the up and leaves its container in place. A failing pre_stop hook is reported, and the stop goes ahead. Either way the message names the hook and its last few lines of output, along with its exit status or, for a hook that couldn't start, why not. Hooks don't run when you start or stop a compose container from the Containers tab or the menu bar.com.compose.* instead of com.container-compose.*. Projects brought up with an earlier version of Orchard or the compose plugin are no longer recognised in the Compose tab. To fix one, take it down with the version that brought it up (or remove its containers), then bring it up again.dns, dns_search and dns_opt, and sent no resolver configuration at all, so the container had no /etc/resolv.conf and every lookup in it failed with connection refused on [::1]:53. Containers created from the Run form get a resolver configuration the same way, so one with no DNS domain set now gets the network's own nameserver as container run gives it.container system property set dns.domain, which those releases no longer have, so Make Default and the DNS Domain picker in Settings failed with "3 unexpected arguments". The default is now an Orchard setting, used for containers you create in Orchard, so changing it needs no administrator password and no restart of the container system. If you haven't chosen one, Orchard uses the [dns] domain from your ~/.config/container/config.toml, and Settings > General shows that value as the container default.kindest/node release of each Kubernetes version from 1.31, fetched from Docker Hub when the sheet opens and pinned by digest. "Custom Image" takes any other reference. Choosing a version only became meaningful with container 1.5.0: until then the plugin configured the control plane for its default version whatever image you passed, so a different image ran mismatched components. For the same reason 1.5.0 rejects a node image with no tag, and Orchard now says so before creating anything. Offline, the menu offers the recommended image and a custom one.--cni). Orchard can't check that a manifest suits the cluster, so the sheet points out that it must fit the pod subnet and links to Apple container's Custom CNI guide.iptables-nft, so the older kernel that an upgraded install keeps now works (apple/container#2120). A node-preparation failure still gets Orchard's own message rather than the CLI's, which names a step that succeeded, but it no longer blames the kernel.container k8s start, so a stopped cluster can only be deleted and created again; Recreate does both in one step. It opens the Create Cluster sheet filled in from the cluster (its name, CPUs and memory, the Kubernetes version and, for clusters Orchard created, the CNI manifest) and deletes and creates when you confirm. A node doesn't record its image tag, so Orchard recovers the version from the image digest, and asks when it can't. Stopped cluster nodes in the Containers list, the menu bar and the command palette no longer offer Start either: a plain start skips the boot, readiness wait and kubeconfig write the old command did, so the node's detail offers Recreate Cluster instead./usr/local, but a Homebrew container looks for plugins inside its own versioned install, so after a brew upgrade a copy in /usr/local still counted as installed although the CLI could not see it, and an install from Orchard went where that CLI never looks. Orchard now asks the CLI itself whether container compose works, and installs into the directory the CLI names in its error. That directory belongs to you under Homebrew, so the install no longer asks for an administrator password there.ENTRYPOINT [""] now run. Orchard used the empty string as the program to launch, so the container failed to start; the command now runs on its own, as the CLI does from 1.5.0.Hi everyone, this release is a huge one personally, and one that's long overdue.
Since Apple launched container, everyone's been looking for Compose support so Docker Compose files can be brought across. The maintainers themselves have turned this down in several issues, pushing it onto the community. I've closed a lot of feature requests because I hoped this would be a core feature, but the maintainers made it very clear they are not going to work on it.
In the early days, many people, including myself, attempted to build something to solve that. However, they were all independent binaries, and still are — requiring their own commands and not portable.
A few releases ago, container added support for plugins, the first of which is an official k8s one — it's the same plugin Orchard itself calls to bring k8s to your box.
So in order to bring these worlds together, I've created a new project, compose, which is a plugin for apple/container that can be installed without Orchard (although it's offered to you with minimal work involved via the UI).
The syntax for compose looks exactly how you would want it:
container compose up --help
I'm opening that up in the hope that other developers can contribute to it and make it the standard way to use Compose with containers. There is no Orchard logic in it — it's a plugin and can be bundled as a Swift library in 3rd party tools such as other UI's or applications, instead of being proprietary.
With this being so new, if you have any feedback or want to contribute, please do!
Check out the project and give this new Orchard release a go — navigate to the Compose view where it will be installed for you, if you wish.
The shell a container terminal opens with is now a setting, taken as written and passed through with its flags, so bash -l gets a login shell that reads your rc files (#107). Which shells an image has and which flags they accept is a property of the image, so there is nothing here for Orchard to enumerate and nothing worth validating beyond refusing to send an empty command. sh remains the default because it is the one shell a minimal image is likely to have, which makes it a safe default rather than a good one.
A Compose tab, for bringing a set of services up and down from a compose file. Projects are found by the labels on their containers rather than by any file Orchard keeps, so a project brought up from the terminal appears here without Orchard having been told anything, and a project Orchard has been shown a file for can be brought up, taken down and reconciled from the window. Running up a second time compares each service against a hash stamped on the container it produced, then creates, starts, leaves alone, recreates or removes each one accordingly, and every step is listed as it happens.
Compose keys that cannot be honoured are never quietly dropped. Adding a project shows what will be ignored, how much ignoring it costs, and the line each key sits on, and the project goes on showing that afterwards: three weeks later nobody remembers what they accepted, and the containers cannot tell them. A file that grows a new unhandled key is put back in front of you before the next up rather than waved through on an older answer. A service that builds is built through the same path as the Images tab, so its BuildKit log shows up under Builds like any other build.
The planning behind the tab is andrew-waters/compose, a Swift package Orchard links and which also backs the container compose plugin, so the terminal and the window cannot drift apart. Two limitations are worth stating up front: without health reporting in the runtime, up starts dependencies before dependents but does not wait for them to become ready, and hostnames have to be unique across every container on the machine, so a service answers to <project>-<service> rather than to its service name.
The Compose tab offers to install the container compose CLI plugin when it is not there. Orchard itself never needs it, because the window links the planner and runs plans over XPC, so this is an offer rather than a fault and the banner is coloured accordingly: every project in the list works with nothing installed. It is worth offering because container's own installer clears the plugin directory on every upgrade (apple/container#1617), so a plugin someone installed by hand goes missing again through no fault of theirs. Installing fetches the latest release, unpacks it, and puts the two files where the CLI looks, with the work shown step by step in a sheet: this downloads 15MB and then asks for an administrator password, and someone about to be asked for their password should be able to see what asked and why. Everything up to the copy happens unprivileged in a temporary directory, so a failure before that point never prompts at all, and a download in flight can be cancelled.
open -na, and -n starts a separate instance: a window in one instance can never join a window in another, so a new window was not a design choice so much as an inevitability, and every container opened left another Ghostty running. Ghostty 1.3 ships a scripting dictionary, so it is now driven the way Terminal.app and iTerm2 already were rather than through open, and all three stop being special cases. Whether the terminal arrives as a tab or a window follows the system-wide "Prefer tabs when opening documents" setting rather than a preference of Orchard's own: you have answered that question once, for every app. macOS defaults that to fullscreen rather than always, so the default is still a window; the difference is that it is now the system's answer rather than Orchard overriding it.container k8s create prepares its node with iptables-nft, which needs a kernel built with nftables; the kernel Apple container recommended before 1.3.0 was not, and container system start skips the kernel download whenever a kernel already exists (apple/container#905), so an install carried forward from an earlier release keeps the old kernel and fails every cluster creation however new the CLI is. Orchard reads the default kernel the CLI records, and when it predates nftables the tab says so and offers "Install Recommended Kernel" in place of the empty state. Creation is still available from there, because the check is a version floor rather than a capability probe and a deliberately chosen custom kernel is nobody's business but yours.container k8s create cluster control-plane,worker, because it is both, and Orchard compared that label to control-plane exactly. The node therefore classified as a worker, the cluster read as stopped while it was running, and everything gated on a running control plane was hidden: the detail header offered Start instead of Terminal (kubectl), Write Kubeconfig, Copy Kubeconfig Path and Load Image, and every resource panel showed "Not running" over live charts. Roles are now read as the list the label can be, so a node that is both counts as the control plane.set -e script whose last two commands are the iptables-nft calls, so an abort there surfaces the accumulated output of the steps that already succeeded: the error names net.ipv4.ip_forward = 1, a sysctl that worked, and never shows the command that failed (apple/container#2120, open with no fix in 1.4.1). Orchard recognises that failure and names the real cause, including the kernel responsible when it can tell.--force, which replaces only a kernel of the same name.Quick none-changelog related note from me - I hope you're finding Orchard useful to you. The last month has seen stargazers on the project double, so it's great to have you here. Please let me know if you're finding this useful and feel free to add any feature requests / bug reports on Github.
The last release had over 3,100 downloads, so please consider giving it a star on Github if you're using it and spreading the word.
And a special thanks to everyone who's contributed to the project so far!
container-apiserver 1.4.1 to 1.4.1.LazyVGrid, and placing a row whose wells hold selectable text re-entered the grid's own placement: the main thread spun in over a thousand nested sizeThatFits frames at 100% CPU with memory climbing about 13 MB a second, and the window never recovered. The same two-column layout is now built eagerly from stacked rows, which six wells cost nothing to lay out up front. The wells also pair up only while the pane is wide enough to carry two and stack below that: at a 1024-point window two columns left a key/value row so little width that its value and Show/Copy controls fell out of the row entirely. Long environment-variable and OCI-label keys additionally cap their column rather than sizing to the longest key without limit, so a namespaced key wraps instead of crowding out its own value.NSURLErrorDomain -1004 console lines per minute on an idle machine. The background tick now backs off to 30s while no provider is detected; while one is detected, the 5s cadence stays so a stopped server disappears promptly, and opening the Models tab or the run form's model bridge still probes immediately.mono2 was rewritten to docker.io/library/mono2 and the backend tried to pull a repository that doesn't exist on the registry (Hub answers 401 for unknown repositories). The reference now resolves against the local image store first - a match runs under the exact reference the store knows (including the implied :latest), and only unmatched references canonicalise into pullable registry form. The "Local image available" hint and the actual run decision now agree.com.apple.container.* and com.orchard.* labels are managed by the runtime/Orchard and don't appear as editable rows.orchard://builds) tracks every image build Orchard has started, and the records survive relaunches (stored as versioned JSON in Application Support, with each log's last 1000 lines). The list shows status, start time, and duration per build, searchable by image name; the detail pane shows the build's request (Dockerfile, platform) and its log - complete while the app runs, trimmed to the last 1000 lines for records restored after a relaunch - live while building and reviewable after, with Cancel Build for in-flight builds and a context-menu Remove Record for finished ones. Once a build's image exists, the detail also mirrors the image view's affordances: a "Launch image" button opening the run-container sheet pre-filled with the built reference, a "Containers using this image" list that jumps to each container, and a confirmed Delete that removes both the build record and the built image (disabled while containers use the image; if the image delete fails, the record is kept). Builds and images cross-reference: the build detail's "View Image" jumps to the built image, and an image produced by an Orchard build gains a "View Build" button jumping to the newest successful build of that reference. Builds run concurrently and survive closing the Build Image sheet; a build still running when the app quits is marked "interrupted" on the next launch, since the CLI process was orphaned and its outcome never observed (the image may still have been built). The registry only lists builds Orchard itself started: apple/container's builder shim exposes just info() and a build stream over its gRPC surface, so BuildKit's own history is not reachable from the host.orchard:// URL scheme. orchard://<tab> opens any sidebar tab (e.g. orchard://dashboard), and orchard://container/<id>, orchard://image/<reference>, orchard://mount/<id>, orchard://machine/<id>, orchard://dns/<domain>, and orchard://network/<id> jump straight to a resource, reusing the same navigation path as the command palette and menu bar (so a target in a still-loading list is selected once it appears). A resource noun without an identifier falls back to its tab, and image references keep their interior slashes.com.apple.container.* bookkeeping labels excluded). With a key selected, the list renders one collapsible section per label value - click a header to fold it - with a count badge, a "No label" bucket for containers missing the key, and a per-group actions menu to start or stop everything in the group. The choice persists, and search/running filters apply within the groups.Dockerfile.dev are discoverable (the build context auto-fills to the Dockerfile's folder), name the image, choose the target platform (Apple silicon linux/arm64 or Intel linux/amd64, defaulting to the host), and optionally build without cache. The build shells out to container build, which starts the BuildKit builder on demand, and the build log streams live into the sheet through the ANSI console; closing the sheet leaves the build running, and a Cancel Build button terminates it. Successful builds refresh the image list automatically.Text per line, which printed colour escape codes as literal garbage and scoped text selection to a single line. Both the detail Logs tab and the log viewer window now share a single NSTextView-backed console: SGR styles (16-colour, bright, 256-colour, truecolour, bold/italic/underline) are rendered, other escape sequences are stripped, carriage-return progress lines show their final state, and click-drag or Cmd+A/Cmd+C works across the whole log. Filtering now matches the escape-stripped text, so a filter term can no longer hit or miss because of bytes inside a colour code, and the console follows the tail only while scrolled to the bottom (scrolling up to read no longer fights the 2s refresh, which previously only auto-scrolled on first load).onDisappear, which also fires when the system stops and the "not running" screen swaps in. After that, nothing polled, so a container system start run in a terminal was never noticed and Orchard stayed on "Container is not currently running" until the power button was clicked. The same dead end was reachable by launching Orchard while the system was stopped. The poll's lifetime now matches the window rather than one branch of the state view, so external stops appear within a poll tick (a stop can still lag while container system stop drains running containers, since the API server answers until it deregisters) and an external start restores the full interface automatically.container image ls -v (#98). Orchard was showing the size of each variant's manifest descriptor — a couple of kilobytes of JSON — instead of the image content, so every image listed as "1 KB"–"3 KB". Variant sizes are now computed the same way the CLI computes FULL SIZE: manifest descriptor plus config blob plus the sum of the compressed layers. The container detail's Image section and the multi-select image cards previously showed the raw index descriptor size as "Size"; both now use the same resolved size as the images list, and the image Technical Details row is relabeled "Index Size (bytes)" since that value genuinely is the index descriptor size.RegistryClient that followed WWW-Authenticate realms without validating the host.https for all registries, matching container 1.3.0's removal of the auto registry scheme. Pulls from localhost or private-IP registries no longer silently downgrade to plain http; a local http registry needs registry.scheme = "http" in the container configuration, same as the container CLI.force_ssl this leaked a socket per probe until the server's accept loop stopped and it served nothing at all. A real provider answers 200 directly, so a redirect is now simply classified as "not a provider" - which also stops a redirecting server from being misreported as a model server.stopSystem() while a transition is already in flight is a no-op at the service layer.Log.xpc so diagnostics aren't blind, and the existing dashboard, container, and machine stats paths are unchanged. No new macOS permission is required.Log.xpc; subsequent outcomes in the same tick fold in silently while the first error is retained, so a single tick with several unreachable containers no longer produces one log line per outcome.container k8s plugin (container 1.2.2+). Node containers are grouped into clusters with per-node role, status, IP, CPUs, memory, and published ports; clusters can be created, started, and deleted from the UI, local images can be loaded into a cluster's containerd, and the kubeconfig can be written or its path copied, with a one-click terminal preconfigured for kubectl. Installs without the plugin get an explanatory state with upgrade guidance.mlx_lm.server, so it previously appeared as a generic "MLX Server"; it's now identified by the owned_by: omlx stamp in its models listing (#72).OPENAI_API_KEY).sleep 3600, not just 3600), so saving doesn't corrupt the container's process configuration. Quoted arguments (sh -c "echo hi") now also survive the round-trip (#42).com.apple.security.automation.apple-events entitlement and a usage description, so macOS shows the Automation consent prompt. If permission is denied, the error now points to System Settings → Privacy & Security → Automation instead of dumping the raw AppleScript error (#64).container now includes a download link if the binary could not be found.mlx_lm.server instances - pick a model and port, choose whether to bind 0.0.0.0 so containers can reach it, with child-process supervision, crash surfacing and log access.OPENAI_BASE_URL at create time - so a containerised app or agent talks to a local model with no hand-configured host networking. Inference runs on the Apple GPU on the host (Virtualization.framework guests have no GPU access)./sbin/init.container CLI.container 1.1.0. Orchard now builds against the 1.1.0 client libraries (previously 0.12.3), which had many breaking API changes across the 1.0 release; container 1.0.0 or later is now required.system property list --format=json from a flat array to a nested object keyed by category, which the parser didn't recognise, so every daemon property read as missing.container daemon properties (Rosetta, image builder/init, kernel, registry).container system property list now returns a JSON array, which the parser didn't recognise, so it read no values at all.999.999.999.999/24); each octet must be 0–255.dev.andon.orchard in Console.app when troubleshooting.container 0.12.x.container binary locations, with a setting to override the path.container XPC API for most operations (no longer shells out to the CLI).container ls -a).container is available.container version.0.6.0 and checks the CLI version for compatibilitycontainer image pull and container image list instead of plural images)Note: this release was also tagged
v1.1.7by mistake.
container tooling, including: