Orchard is an easier way to manage your compose stacks. Take a look

Compose,
for Apple Containers

Bring a stack up

container compose up

Install the plugin

git clone https://github.com/andrew-waters/compose
cd compose
make build && sudo make install

If you are subscribed to #230, #1846 or #239, you have been waiting for the runtime to grow a compose command. It is not going to. Compose is out of scope upstream, and plugins are the route container’s maintainers pointed at instead.

So this is a plugin. It installs into container’s plugin directory, which makes container compose up the real command under the real CLI. Not a second binary with its own name, its own flags and its own idea of what a project is.

Coming from Docker Compose

Capability Docker Compose container compose
Carries over unchanged
image, build yes yes args, target and dockerfile included; the build itself shells out to container build
environment, env_file, .env yes yes interpolation included, with a line and column on every error
command, working_dir, labels, container_name yes yes
ports yes yes short and long form; a clash with a running container stops the plan before anything is created
Bind mounts under volumes yes yes
deploy.resources yes yes a VM cannot have half a core, so cpus round up
depends_on, list form yes yes dependencies start first
dns, dns_search, dns_opt yes yes
Project naming, -f, -p, --dry-run yes yes name from the file, then the directory
Works differently here
Reaching a service by name yes db partial hostnames are unique per machine, so it is myapp-db rather than db (#1809)
depends_on conditions yes service_healthy not yet order only; waiting for ready needs health in the runtime (#1502)
healthcheck yes not yet a container cannot report health, so probes would have to run here
Named volumes yes not yet bind mounts today
Several networks per service yes not yet one attachment per container
entrypoint yes not yet the create surface overrides cmd, not entrypoint
profiles, include yes not yet
pull_policy, platform yes partial an image is pulled when it is missing, and everything on this stack is linux/arm64
ps, logs, exec, stop, start yes not yet only up and down here, with container’s own ls and logs covering the rest meanwhile
Not possible on this runtime
restart yes no nothing restarts a container that exits (#2142)
user yes no the create surface cannot set the process user
cap_add, cap_drop, privileged yes no capabilities are not settable and there is no privileged mode
devices, tmpfs, ulimits yes no no device passthrough, no tmpfs mounts, no settable limits
secrets, configs yes no container mounts neither
extra_hosts yes no the hosts file is not writable at create
network_mode yes no only user-defined networks are attachable

Nothing in the last band is silently dropped. Each one refuses the file by name, on the line it sits on. The middle band is where a contribution lands today.

What you actually getwhen you install it

Not a wrapper that shells out to container run in a loop. A parser, a dependency graph and a planner, with the runtime call at the very end.

Plugin, not binary

The real command

container compose up, container compose down. It lands in container’s plugin directory and appears in container --help beside everything else. Your muscle memory, your scripts and your CI all keep working the way they read.

No silent drops

It refuses what it cannot honour

Every compose key is classified: honoured, deferred, or impossible until the runtime grows a feature. Anything behavioural that would not happen stops the run, named, with the line it is on. Nothing is quietly ignored.

Idempotent

Containers are the only state

There is no state file to drift or delete. A second up compares each service against the hash stamped on the container it produced, then creates, starts, leaves alone, recreates or removes accordingly.

Testable

The planner is a pure function

A compose file plus a snapshot of what exists goes in; an ordered list of operations comes out. No subprocesses, no XPC, no daemon. Ordering and reconciliation are where compose tools go subtly wrong, and here they are covered by tests that start nothing.

Built to be embedded

A package, not just a binary

The model, the parser and the planner ship as a Swift package with no runtime client in them. A GUI can link it and execute plans over XPC directly, without shelling out to this plugin and without the plugin being installed at all.

One dependency

Yams, and nothing else

The libraries depend on the YAML parser apple/container already uses, so linking them adds nothing to your graph. It reports a line and column for every node, which is what lets a refusal point at the line it is refusing.

~/work/myapp
container compose up
myapp  ./compose.yaml, 3 services
  db   create  no container exists
  api  create  no container exists
  web  create  no container exists
[1/9] create network myapp
[2/9] pull postgres:17
[3/9] create container myapp-db
[4/9] start myapp-db
[5/9] build myapp-api from /Users/you/work/myapp/api
[6/9] create container myapp-api
[7/9] start myapp-api
[8/9] create container myapp-web
[9/9] start myapp-web

It would rather stopthan lie to you

a file this stack cannot honour
container compose up
myapp  ./compose.yaml, 4 services
error: ./compose.yaml asks for 2 things this cannot do
  8:14: `restart` in service `db` is not honoured: container has no restart policy;
        a container that exits stays exited
  15:11: `user` in service `api` is not honoured: the create surface cannot set the
         process user
error: nothing was created. Remove or change those keys and run again.

Keys that cost nothing to ignore, such as an obsolete version, are printed as notes and stepped over. The difference is a severity carried per key, not a judgement made at the moment of refusal.

Three things to knowbefore you install it

Nothing waits for ready

up starts dependencies before dependents, but a container cannot report health, so it cannot wait for one to become ready. Until #1502 lands, your app still needs to retry its first connection.

A service answers to <project>-<service>

Hostnames have to be unique across every container on the machine, so db is reachable as myapp-db, not db. Compose’s own naming needs per-network namespacing: #1809.

Upgrading container wipes plugins

The installer clears the plugin directory on every upgrade (#1617), so expect to reinstall afterwards. Keep the two lines above to hand; an installer that asks for admin once is the fix, and it is not here yet.

Install it

A plugin is a directory with a config file and a binary. The plugin directory is root owned, which is what the sudo is for.

Recommended

From source

Swift 6.2 is all it takes. Clone the repo, then build and install: the first stamps the version from the tag, the second puts the plugin where the CLI looks for it.

make build
sudo make install

Prebuilt

From a release

The tarball is the plugin directory exactly as it installs, so unpacking it into place is the whole install. Not notarised yet, so macOS may quarantine it on the way in.

tar -xzf compose-*.tar.gz
sudo cp -R compose \
  /usr/local/libexec/container-plugins/

Then check

Confirm the CLI found it

A plugin the CLI cannot see fails quietly, with no error worth reading. One command settles it: the abstract and both subcommands should print.

container compose --help

Requires an Apple silicon Mac on macOS 26 and apple/container 1.4.1 or later.

One of these, not five

Everyone who wanted compose on this runtime went and built a piece of it. That is four or five parsers, four or five planners, and four or five sets of the same subtle ordering bugs, all against a runtime that is still moving underneath them.

This is the version built to be shared: a plugin for the CLI, a package for anyone’s GUI, and a coverage table that says plainly what is missing. If you have been following those threads, the most useful thing you can do now is bring your compose file, find the key it refuses, and file it.