-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
dcon is a thin translation layer with no daemon of its own. It is one ~5.3 MB static Go binary that speaks Docker on the front and shells out to Apple's container CLI on the back. Every command you run (run, ps, build, compose up) ends up as one or more container invocations; dcon's job is to translate the arguments going in and re-render the output coming out in Docker's format.
This page covers the pipeline, where the time actually goes, how the warm pool and compose parallelism fit, what dcon doctor checks, the daemonless state model, and the source layout. For the warm pool's full behaviour see Warm Pool; for the measured numbers and methodology see Benchmarks & Comparison; for the full command list see Command Parity.
flowchart LR
A["you / CI / Makefile<br/>docker run · compose up · build"] -->|docker-style args| B["dcon<br/>(Go / cobra, ~5.3 MB static)"]
B -->|container args| C["Apple container CLI"]
C --> D["container-apiserver<br/>(launchd, on-demand) + plugins"]
D -->|Virtualization.framework| E["per-container<br/>Linux microVM"]
Reading it left to right:
- You / CI type a Docker command (or a script / Makefile does).
- dcon (cobra-based) parses it, translates Docker flags to their
containerequivalents, and shells out. There is no dcon background process: the binary runs, talks tocontainer, and exits. - Apple
containerCLI is the front end to the real engine. - container-apiserver runs under launchd, on demand. It (plus its plugins) drives the boot.
- Virtualization.framework is what creates the VM.
- Each container gets its own Linux microVM, not a shared VM with namespaces. This is the source of both dcon's stronger isolation and its higher cold-start cost.
All backend calls funnel through internal/runtime (runtime.go). It locates the binary (DCON_CONTAINER_BIN env override, then PATH, then the well-known /usr/local/bin/container) and exposes a few execution helpers:
| Helper | Use |
|---|---|
Run |
interactive / streaming commands (run, exec, logs, build) — stdio inherited |
RunWith |
same, but with redirected streams (cp to stdout, export -o -) |
Capture |
capture stdout, still stream stderr so progress stays visible |
CaptureSilent |
capture both streams, fold stderr into the error — used when dcon must inspect output without leaking container's native formatting |
CaptureJSON |
CaptureSilent + json.Unmarshal into a model |
Set DCON_DEBUG=1 (or =true) to echo every underlying container <args> line to stderr, so you can see exactly what dcon translated your command into.
The finding from profiling on the real machine (Apple silicon Mac16,12, macOS 26, Apple container 1.0.0): dcon adds essentially zero overhead. The cost is Apple's per-container microVM cold boot.
container run --rm alpine echo … ~700 ms
dcon run --rm alpine echo … ~690 ms
Within noise, identical. The Go translation layer does not show up in the timing; there is no point optimizing dcon's code path for cold start.
The lifecycle splits cleanly:
| Step | Cost | What happens |
|---|---|---|
container create |
~40 ms | allocate the container record |
container start |
~650 ms | the boot: kernel + init + vminitd + network |
Pre-creating containers does not help: the ~650 ms lives entirely in start. There is no cheap window to exploit before the boot.
The apiserver serializes VM boots. Four concurrent container run -d come out only ~11 % faster than running them sequentially. Throwing concurrency at the boot path does almost nothing.
This shapes everything downstream:
The only lever on start latency is to pre-pay the boot off the critical path. You cannot make a boot faster, and you cannot run many boots in parallel. You can only do them ahead of time. That is the warm pool.
exec into an already-running VM costs ~60–90 ms. That is the warm-pool floor, bounded by the container CLI's own Swift startup (~70 ms), not by dcon. So ~90 ms is roughly as fast as this backend gets per workload.
The warm pool follows from the profiling above: since the ~650 ms boot can't be made faster or parallelized, dcon pays it in the background, ahead of time.
A pool member is a pre-booted, single-use microVM. A simple eligible run execs the workload into a ready VM (~90 ms) instead of cold-booting (~700 ms). Each member is handed out exactly once and then destroyed, so every run still gets a pristine, never-used microVM: the per-container isolation is identical to a cold run. The boot cost simply moved off your critical path.
That ~90 ms beats a shared-VM engine's start while keeping per-container isolation, which an always-warm shared VM can't claim.
Eligibility, commands (dcon warm / warm ls / warm prune), env knobs (DCON_WARM, DCON_WARM_DEPTH, DCON_WARM_TTL), the cold==warm correctness handling (ENTRYPOINT/CMD resolution), and the daemonless replenish model are all detailed on the Warm Pool page. The short version: only flags that exec can faithfully reproduce keep a run on the fast path; anything bound at VM-boot time (bind mounts, ports, resource limits, custom networking, --name, --entrypoint, …) transparently takes the normal cold path.
compose up is dcon's other place to overlap work, implemented in cmd/compose.go on top of internal/compose's Project.Levels().
The model is dependency-level concurrency:
-
Project.Levels()topologically groups services into levels.depends_onordering is preserved across levels. - Within a level (services with no ordering constraint between them) dcon brings them up concurrently, each in its own goroutine.
- A semaphore caps the fan-out at
DCON_COMPOSE_PARALLEL(default 8;<= 0means unlimited) so a wide stack can't try to boot dozens of microVMs at once. The cap and default live inparallelLimit(). - A level fully completes (
wg.Wait()) before the next level starts; the first error in a level aborts the bring-up.
Because Apple serializes VM boots (see above), a pure-boot stack gains only modestly from this, roughly the same ~11–16 % ceiling that concurrent run -d hits. The compose parallelism does not dodge boot serialization.
It pays off on build- and pull-heavy stacks (compose up --build): the network- and builder-bound steps for independent services overlap, and those are not serialized the way boots are. Don't expect large speedups on a stack that is just booting prebuilt images.
dcon doctor (also dcon system doctor) is a one-shot setup diagnostic, implemented in cmd/doctor.go. It probes the environment and prints an aligned report with ✓ / ! / ✗ lines, each non-OK line carrying a ↳ remediation hint. It exits non-zero on a hard failure, so it's CI-friendly.
What it checks, in order:
| # | Check | OK means | If not |
|---|---|---|---|
| 1 | Apple container CLI | present, with version | fail: install from apple/container releases or brew install --cask container; remaining probes are skipped |
| 2 | Backend services |
container system status reports running
|
fail: dcon system start
|
| 3 | Guest kernel | a kernel exists under the app-support kernels/ dir |
warn: dcon system kernel set --recommended (read-only commands work without it; booting containers needs it) |
| 4 | Image builder |
container builder status shows running
|
warn: starts on first dcon build, or dcon builder start
|
| 5 | docker drop-in |
docker on PATH resolves to this dcon binary |
warn: alias docker=dcon or make link-docker
|
| 6 | Warm pool | informational: ready-member count + mode (off / auto / manual) |
always OK |
Check 1 is the gate: if the backend binary isn't found, dcon stops there because every other probe would be meaningless. The formatting half (renderChecks) is unit-tested separately from the integration half (gatherChecks).
dcon runs no persistent process of its own. Each invocation is a short-lived CLI that talks to the Apple backend and exits. The only always-available background piece is Apple's apiserver (launchd, on-demand), not dcon.
The warm pool is the one feature that needs cross-invocation state, and it stays daemonless:
- State file:
~/Library/Application Support/dcon/pool.json, aflock-guarded JSON file listing only the available pool members. Claiming a member pops one atomically, which is safe across concurrent processes. - Background work (boot / destroy / replenish) runs as detached (
setsid) processes that outlive the CLI that spawned them. No long-running supervisor. - Leak safety: members are label-tagged
dcon.pool=1, so any VM orphaned by a crash is reaped bydcon warm prune. If a claimed VM turns out to be already gone, the run transparently falls back to a cold boot.
Note the two distinct app-support roots: dcon's own state lives under …/Application Support/dcon/, while Apple's backend state (kernels, etc., which doctor reads) lives under …/Application Support/com.apple.container/ via runtime.AppRoot().
| Path | Responsibility |
|---|---|
main.go |
entrypoint; wires up the cobra command tree |
cmd/ |
one file per command group (run, compose.go, images.go, warm.go, doctor.go, …) |
internal/runtime/ |
locate and drive the container binary (runtime.go) |
internal/dockerfmt/ |
JSON models + Docker-style table/template rendering (ps/stats tables, --format) |
internal/compose/ |
compose parser + service→container translation; Project.Levels() for dependency ordering |
internal/pool/ |
the warm pool: state file, claim/replenish, eligibility |
scripts/ |
install.sh, bump-version.sh, coverage, bench.sh
|
.github/workflows/ |
CI (test + coverage) and the tagged Release pipeline |
runtime is the only package that knows how to invoke container; dockerfmt is the only package that knows Docker's output shapes; compose and pool are self-contained features built on those two primitives.
make build # build ./dcon (version injected via ldflags)
make test # go test ./...
make cover # coverage summary
make bench # dcon vs docker benchmark → scripts/bench-results.md
make vet fmt # static checks + formattingCI runs vet + race tests + coverage + a cross-build on every push/PR, and auto-refreshes the coverage badge on main. Coverage is currently ~45.8 %.
Cutting a release is one command:
scripts/bump-version.sh patch # or: minor | major | vX.Y.ZIt tags and pushes. The Release workflow then builds darwin/arm64 + darwin/amd64 binaries, generates checksums and notes, and publishes a GitHub Release, which install.sh consumes for the curl … | bash one-liner.
See also: Warm Pool · Benchmarks & Comparison · Command Parity · Home · the README and the cookbook.