A composition layer over Apple's container,
for local multi-service stacks on a Mac.
container deliberately stops at the primitives — run, create, start, stop,
delete, networks, volumes, labels, JSON inspect, logs, exec — and has no compose.
This is that layer: one TOML file, a plan you can read before anything runs,
ownership carried on labels, and a foreground supervisor, because the runtime has
no restart policy of its own.
It is not a Compose clone and does not read Compose files. Anything the runtime
cannot honour is refused rather than accepted and ignored — see
docs/02-stack-file.md. It only
ever stops and deletes what it created: delete --all, volume prune and
network prune are never used.
version = 1
name = "minimal"
[services.echo]
image = "docker.io/library/alpine:latest"
command = ["echo", "hello"]$ container-compose plan
PLAN minimal config c3431d7551ae4ef5 (/path/to/stack.toml)
keep network default mode default, subnet 192.168.64.0/24
create container minimal-echo docker.io/library/alpine:latest
start container minimal-echo docker.io/library/alpine:latest
3 steps. Run `container-compose up` to apply.plan --commands prints the exact container argv instead, and plan --json
the same thing as a document.
There is no packaged binary yet; the flake exports the devshell, not a package. From the repo root, in a shell with that devshell loaded:
direnv allow . # once per checkout; afterwards `cd` loads it
cargo build --release
./target/release/container-compose --helpWithout direnv, nix develop -c cargo build --release does the same.
- macOS on Apple silicon, with
container1.6.0 or later running (container system status). - A container DNS domain, if the stack's services name each other — which most do.
Register the domain once per machine:
# ~/.config/container/config.toml
[dns]
domain = "test"container system stop && container system start
sudo container system dns create testWithout it, a service dialling api.test fails at its first lookup, which looks
like a broken image rather than missing host setup. plan warns when a stack
names a service and no domain is registered. Containers created before the
domain was registered do not get a name — down then up after setting it.
| command | what it does |
|---|---|
config |
print the resolved stack, without contacting the runtime |
plan |
print what up would do, without running anything; --commands, --json |
up |
create and start in dependency order, then supervise until Ctrl-C |
down |
stop and delete what this project owns; --volumes, --timeout |
ps |
what is running, with health; --probe, --json |
top |
ps kept live in the terminal, with CPU and memory, restarts, changes and the selected service's logs |
stats |
CPU, memory, network and disk per service over a window, from the supervisor's history; --since, --json |
logs, exec, run |
the runtime's own, scoped to the stack |
build, pull |
the images the stack declares |
up -d exists, but it refuses a service with a restart policy: a policy that
only worked while a supervisor was attached would be a lie.
The whole surface above works and is exercised against the runtime. container
1.6.0 on macOS 27.0.1 arm64 runs the midnight-lab ledger-9 devnet — node,
indexer and prover — with the indexer syncing through container-name DNS and
Ctrl-C stopping all three in reverse order.
The same behaviour is covered without a runtime: 162 tests — most of them
driving dev/fake-container, which records every argv — plus one #[ignore]d
smoke test that runs a real three-service stack
(cargo test -p container-compose-cli --test smoke -- --ignored --nocapture).
CI runs fmt, clippy and the suite on [self-hosted, netcup-arm].
Read docs/README.md first. The design is
01–05;
06 is what is not built yet, and
07 is the record of what the runtime actually does,
including the findings that changed the design.