Skip to content

Add Apple container runtime support#205

Draft
slang25 wants to merge 1 commit into
microsoft:mainfrom
slang25:slang25/wslc-pr-194-learnings
Draft

Add Apple container runtime support#205
slang25 wants to merge 1 commit into
microsoft:mainfrom
slang25:slang25/wslc-pr-194-learnings

Conversation

@slang25

@slang25 slang25 commented Jul 3, 2026

Copy link
Copy Markdown

Add Apple container runtime support

Summary

Adds Apple's container CLI as a container runtime alongside docker and podman, so aspire run can orchestrate containers on macOS (Apple Silicon) using the native Apple runtime — no Docker Desktop required.

$ dcp info --container-runtime container
{"version":"dev","containers":{"runtime":"container","hostName":"192.168.64.1","installed":true,"running":true}}

Motivation

Aspire already supports container as a publishing runtime; DCP was the blocker for the run path (--container-runtime container was rejected). The Apple runtime differs structurally from Docker/Podman — each container runs in its own lightweight VM with a routable IP — and several Docker-isms simply don't exist. This PR adapts DCP's orchestration model to those constraints. All CLI behavior was validated against container CLI 1.0.0 on a real machine.

What's included

New container orchestrator (internal/applecontainer)

  • Full ContainerOrchestrator implementation: container/volume/network/image lifecycle, exec, log streaming, and Apple-specific JSON parsing (the schemas are unrelated to Docker's).
  • Polling-based events: the CLI has no events command, so container/network events are synthesized by diffing runtime snapshots (2s/5s), keeping DCP's event-driven reconciliation working.
  • Per-object inspection: Apple inspect commands are all-or-nothing; multi-object inspections are issued one at a time to preserve DCP's partial-success contracts.
  • Synthesized log timestamps (logs has no --timestamps), client-side label filtering (ls has no --filter), host-port allocation for dynamic ports (-p requires an explicit host port), and created-vs-exited disambiguation (the runtime reports both as stopped; distinguished via startedDate).
  • Label value encoding: the CLI rejects --label values containing = (which DCP's mount-tracking labels use); such values are transparently base64-encoded and decoded when read back.
  • dcp info reports gateway-based hostName, versions in diagnostic mode, and an actionable error (container system start) when the system services are stopped.

Single-network mapping (SingleNetworkOrchestrator capability)

The runtime cannot connect/disconnect containers after creation, and containers can only reach their own network's gateway (verified: another network's gateway is unreachable), so per-app custom networks would break container→host connectivity via the single hostName DCP reports. Instead, DCP-managed ContainerNetworks map onto the built-in default network:

  • The network controller reflects the default network into the ContainerNetwork status instead of creating anything; deletion is a no-op.
  • Containers attach at creation time (building on Create containers with initial network attachments #198), ConnectNetwork verifies attachment (reporting ErrAlreadyExists, which callers treat as success), and the attached-container list for network inspection is synthesized from the container list.
  • Containers on the default network have routable IPs and can reach each other directly, and reach the host via the gateway IP.

DNS forwarder for network aliases (cmd/dcpdns, internal/dcpdns)

The runtime has no --network-alias equivalent and its gateway DNS serves no records for container names (verified empirically; container system dns requires sudo and only configures host-side resolution). Without aliases, container-to-container references and tunnel-proxy DNS names cannot resolve. DCP therefore provides name resolution itself:

  • A small, dependency-free DNS forwarder (dcpdns) answers A-record queries for names DCP publishes and relays everything else verbatim to the network gateway, so regular resolution is unaffected. Ships as dcpdns_c (static Linux binary) alongside dcp, mirroring dcptun_c.
  • The orchestrator builds a FROM scratch image from it (once per DCP version, same pattern as the tunnel-proxy client image) and runs one forwarder container per DCP instance, labeled with the standard creator-process labels so the resource harvester removes it when the DCP process exits.
  • Every container DCP creates is pointed at the forwarder via --dns. Records (container name + network aliases → container IP) are published by atomically rewriting a hosts-format file in a host directory bind-mounted read-only into the forwarder; the forwarder re-reads it with bounded (~1s) staleness. Records are retracted on stop/remove.
  • Fully degradable: if the forwarder cannot be provisioned (e.g. builder unavailable), containers are created without --dns and keep gateway-only resolution — alias lookups fail but nothing else regresses.

Verified live end-to-end: a container created before an aliased container resolves the alias to the right IP, container names resolve, external names resolve through the relay, and records are retracted on container removal.

Baked file injection + content-addressed derived-image cache

The runtime can only copy files into a running container, so the created-then-copy flow (dev certs, WithContainerFiles) cannot work. Instead:

  • New CreateFilesRequiresRunningContainer() on the CreateFiles interface (false for Docker/Podman). When true, the container controller bakes configured files and PEM certificates into a derived image as tar layers before creation, so they are present (with exact ownership/permissions) before the entrypoint runs.
  • Derived images are tagged content-addressably — repo:tag-dcp-<sha256(baseImageID + layerDigests)> with deterministic layer mod-times — and reused on cache hits, so unchanged inputs never rebuild across runs. This also benefits Docker/Podman image-layer users (previously the derived tag was lifecycle-key based).
  • Shared BuildCreateFilesTar helper produces the tar archives (usable as copy payloads or image layers).

Note: this mechanism intentionally mirrors the approach in #194 (wslc), which has the same runtime constraint. Whichever PR lands second should reconcile the (functionally identical) shared pieces: the CreateFiles interface method, BuildCreateFilesTar, and the derived-image cache in applyContainerImageLayers.

Known limitations (inherent to Apple container 1.0)

  • No exit codes for detached containers — the runtime does not expose them, so DCP reports 0. (container start --attach does propagate exit codes; a follow-up could exploit that with an attached observer process.)
  • No restart policies, health checks, or attach — omitted from generated commands (DCP-level health probes are unaffected); the Terminal feature returns a clear unsupported error.
  • CreateFiles on a running container requires tar in the image (only relevant for direct API use; the controller path uses baked layers).

Testing

  • Unit tests: Apple JSON deserialization fixtures captured from the real CLI, command argument construction, state mapping, label encoding, polling-watcher event synthesis (via the simulated process executor), BuildCreateFilesTar, the derived-image cache build/skip matrix, DNS wire-format parsing/answering, records-file reloads, an in-process forwarder end-to-end test (authoritative + relay paths against a fake upstream), and DNS record publishing/retraction.
  • Live test (DCP_TEST_APPLE_CONTAINER=1, macOS + Apple container required): full lifecycle against the real runtime — pull, run with ports/volumes/labels, list/filter, inspect, exec, file injection, log capture, derived-image bake via the real builder, network verification, DNS alias resolution between containers via the real forwarder, stop/remove, volume cleanup.
  • go build ./..., make lint (0 issues), full unit + integration test suite passes.
  • dcp info --container-runtime container validated in healthy, stopped-services, and diagnostics modes; runtime auto-detection prefers a running Apple runtime only when Docker/Podman are unavailable (docker remains the tie-break default).

Why draft

  • End-to-end aspire run validation (acceptance criteria: endpoint reachable, service discovery, cleanup, dashboard logs) still needs an Aspire build that passes --container-runtime container through to DCP.
  • Coordination with Add support for WSL Containers runtime #194 on the shared create-files/image-cache abstractions.

Adds Apple's 'container' CLI (https://github.com/apple/container) as a
container runtime alongside docker and podman, so 'aspire run' can
orchestrate containers on macOS using the native Apple runtime.

- New internal/applecontainer orchestrator driving the Apple container
  CLI: container/volume/network/image lifecycle, exec, log streaming
  (with synthesized timestamps), polling-based container/network events
  (the CLI has no 'events' command), per-object inspection (Apple
  inspect commands are all-or-nothing), and Apple-specific JSON parsing.
- DCP-managed networks map onto the built-in 'default' network via the
  SingleNetworkOrchestrator capability: the runtime cannot create
  attachable custom networks (containers can only reach their own
  network's gateway, which would break container-to-host connectivity).
- Network aliases are resolved by a small DNS forwarder (cmd/dcpdns)
  that DCP runs in a container on the default network: every container
  is pointed at it via --dns, DCP publishes container-name and alias
  records to it through a bind-mounted hosts file, and unknown names
  are relayed to the network gateway. The runtime itself has no alias
  support and serves no DNS records for container names.
- Files and PEM certificates are baked into a content-addressed derived
  image at creation time (CreateFilesRequiresRunningContainer), since
  the runtime can only copy files into a running container. Derived
  images are cached by base image ID + layer digests, so unchanged
  inputs skip the rebuild across runs.
- Label values containing '=' are transparently base64-encoded (the
  Apple CLI rejects them) and decoded when read back.
- 'dcp info --container-runtime container' reports installed/running
  status, gateway-based hostName, and actionable errors when the
  system services are stopped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@slang25 slang25 force-pushed the slang25/wslc-pr-194-learnings branch from 7999c92 to c6d992e Compare July 3, 2026 18:42
@danegsta

danegsta commented Jul 4, 2026

Copy link
Copy Markdown
Member

@slang25 thanks for looking into Apple container support. I don’t think we’re ready to enable it in DCP yet because there are still several runtime gaps that affect core Aspire/DCP scenarios.

The main blockers are around feature parity with Docker/Podman: container-to-container DNS/network aliases, health checks, restart policy support, attach/terminal behavior, reliable detached exit-code reporting, and some file-copy/image customization flows. Some of these can be partially worked around in DCP, but not all of them are clean or complete enough yet.

We did already make progress in DCP with #198, which creates containers with their initial network attachments instead of creating first and attaching later. That helps with runtimes where networking has to be decided at creation time, including Apple container.

There are also upstream issues/PRs that would close some of the remaining gaps, such as Apple container DNS/network alias support (apple/container#1809, apple/container#1839).

I’m also planning to try opening a PR against Apple container’s cp command to add tar stream/file archive support. That would let DCP handle some file injection/customization paths without depending on tar existing inside the target image.

So overall: Apple container support is still something we want to keep moving toward, but I’d prefer not to claim support until the remaining runtime gaps are either fixed upstream or we have robust DCP-side fallbacks.

@slang25

slang25 commented Jul 5, 2026

Copy link
Copy Markdown
Author

@danegsta thanks, that makes sense. I'll hold off here.

Funnily enough I did some similar work to add the tar functionality for socktainer, which has the same issues we are trying to solve here, which is "how do we bridge the gaps between how Apple containers work vs Docker".

What I've added in this PR mirrors socktainer, which is to add a dns forwarder, however a baked in solution would be nicer for sure.

@danegsta

danegsta commented Jul 5, 2026

Copy link
Copy Markdown
Member

I'm hoping they'll be open to a PR that uses native interop to perform the required tar operations. I've been planning on putting together a tracking issue next week for all the features we need that are missing so we have a relatively definitive list. Obviously some like missing events support are more annoyances than full blockers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants