Skip to content

Evaluate Apple container machine as a macOS backend (alongside or instead of Lima) #291

Description

@hbrodin

Summary

Evaluate replacing — or supplementing — the macOS Lima backend with Apple's container tool and its container machine abstraction. Both run guest VMs on Apple's Virtualization.framework, so this is a question of which orchestration layer coop delegates to on macOS, not a change to the virtualization technology itself.

Empirical testing on a real macOS 26.5.1 / Apple-Silicon host (notes below) shows an Apple container backend is viable as an opt-in third backend on macOS 26+. This issue lays out why we'd consider the shift, what we'd gain, and what it would cost.

A note on scope. The relevant comparison is Apple container vs Lima, since that's what it would replace or supplement on macOS — not vs Firecracker (the Linux backend). Lima already runs a full Ubuntu kernel with CONFIG_NF_TABLES and CONFIG_IP_NF_RAW, so the two kernel workarounds coop maintains (iptables-legacy, DOCKER_INSECURE_NO_IPTABLES_RAW) are Firecracker-only and already absent from the Lima path (src/lima.rs:1373). Apple container matches Lima here — it does not improve on it. The honest gains over Lima are first-party tooling and architecture fit; the kernel-workaround story is irrelevant to this decision and is called out below only to forestall the apples-to-oranges framing.

Assumed floor: macOS 26+ (Apple Silicon). Older-macOS support would have to be dropped for an Apple-container-only backend; keeping Lima as a fallback avoids that.

Background

Today coop selects a backend at compile time: Lima on macOS (wrapping Virtualization.framework via limactl), Firecracker on Linux. The shared SshTarget/Backend abstraction in src/backend.rs means all SSH-based operations — config injection, workspace sync, VS Code, port forwarding — are backend-agnostic. A new macOS backend only has to satisfy the same contract: create named VMs, give each a host-reachable SSH endpoint, support live host-directory mounts, and tear down cleanly.

Apple container reached 1.0.0 on 2026-06-09. container machine boots a Linux VM (Apple's Containerization kernel) into which we can bake an Ubuntu+systemd guest image — the same golden-image pattern we already use for Lima.

Why consider the shift — what we'd gain

  1. First-party, Apple-maintained tool. container is Apple's own project tracking Virtualization.framework directly, versus Lima as a third-party wrapper. Fewer layers between coop and the platform, and a tighter coupling to the OS it targets.

  2. Live mounts at full virtiofs parity. container run --volume /any/abs/path:/dst (or --mount source=…,target=…) live-mounts any absolute host directory — bidirectional virtiofs, changes visible immediately on both sides, matching Lima's live-mount semantics. No $HOME restriction (verified in Parser.swift/validateMount). Only constraint: the host path must exist and be absolute (no auto-create).

  3. The SshTarget/Backend abstraction holds. Each machine gets a host-reachable private IP (192.168.64.x via vmnet). Baking sshd + a pubkey into the guest image (the golden-image pattern we already use) and reaching it over the private IP keeps the entire shared SSH layer intact — no coop-side rework.

  4. Concurrent multi-machine boot works. coop runs several named VMs at once. Created coop-a and coop-b simultaneously; both showed running with distinct IPs. set-default only governs whether -n can be omitted, not exclusivity.

What it would cost — drawbacks and risks

  1. macOS 26 floor. An Apple-container-only macOS backend drops pre-26 support. Mitigation: keep Lima as the macOS fallback and add container as an opt-in third backend, paying the cost of maintaining two macOS paths instead of one.

  2. 1.0.0 maturity / lifecycle flakiness. container is freshly 1.0.0. In testing, stop/delete were clean and instant, and of the lifecycle bugs originally flagged only #861 remains open (the rest are closed; #977 was a fixed old-version regression, not a live --rm data-loss bug). Still, this is young software and the lifecycle surface deserves caution.

  3. Machine IPs are not stable across reboots. Observed drift (.6→.7, .8→.9) on restart. coop must re-resolve the IP via container machine inspect on each start rather than caching it. This is a real change to how coop tracks an instance's SSH endpoint.

  4. container machine run exec is flaky and quirky — intermittent Operation not supported by device during boot races; it word-splits the command (sh -c "echo AAA" runs echo with AAA as $0); and it does not forward piped host stdin. coop already drives guests over SSH, so this is fine for runtime — but provisioning steps cannot rely on machine run to inject files. All provisioning must be baked into the golden image or pushed over SSH.

  5. No disk-size knob — coop resize has no analog. machine create/machine set expose only cpus, memory, home-mount. Disk is grow-on-demand EXT4 (sparse on host APFS; the DISK column grew 75M→700M+ as content unpacked). coop's resize command can't be implemented against container. Likely moot given grow-on-demand, but it's a feature-parity gap to acknowledge.

  6. --publish loopback forwarding is buggy on macOS 26.1–26.3 (#919, #1321). Use the direct private IP, not -p. A host-side macOS "Local Network" permission gate can apply (not hit in the CLI context tested).

Empirical confirmation (macOS 26.5.1, Apple Silicon, container 1.0.0)

All five originally-open questions now have verdicts from a real host:

Question Verdict
Concurrent multi-machine boot ✅ Two machines running with distinct IPs
dockerd in Ubuntu+systemd, end-to-end docker run hello-world pulled+ran; raw table present (full kernel, like Lima — no FC-style workarounds needed)
SSH over the private IP ✅ Ping + TCP/22 + key auth as host-matching user, end-to-end
Disk sizing / resize ✅ No disk knob; grow-on-demand EXT4 (resize moot)
Lifecycle robustness on 1.0.x ✅ Clean stop/delete; only #861 still open

Two corrections to earlier desk research: the disk is EXT4 grow-on-demand images sparse on host APFS (not "APFS thin-provisioned" per-machine volumes); and #977 is fixed, not a live data-loss bug.

Recommendation

The case here is weaker than the Firecracker comparison would suggest, because vs Lima there is no kernel-workaround win — both run full kernels. The genuine upside over Lima is first-party tooling and tighter OS coupling, with mount/SSH parity (not improvement). Against that sit real costs: a macOS 26 floor, 1.0.0 lifecycle maturity, per-reboot IP churn, provisioning that can't use machine run, and no coop resize analog.

Given that the upside over Lima is modest and the costs are concrete, the recommendation is to treat this as exploratory / opt-in only — worth a prototype third backend behind the existing compile-time selection (Lima stays the macOS default), but not a reason to drop Lima. Revisit if Apple container matures past 1.0.x or if a future need (e.g. dropping the third-party Lima dependency, or a capability Lima lacks) tips the balance.

Implementation notes for whoever picks this up

  • New backend module mirroring src/lima.rs, wired into the Backend enum in src/backend.rs.
  • Golden image: ubuntu:24.04 + systemd + docker.io + openssh-server, pubkey baked into /etc/skel.
  • Re-resolve the guest IP via container machine inspect on every start; never cache it.
  • Drive all guest interaction over SSH; never depend on container machine run for provisioning or stdin.
  • No resize implementation; document the grow-on-demand behavior instead.
  • Reuse direct private-IP connectivity; avoid --publish.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions